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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb5
-rw-r--r--spec/config/inject_enterprise_edition_module_spec.rb2
-rw-r--r--spec/config/object_store_settings_spec.rb2
-rw-r--r--spec/config/settings_spec.rb16
-rw-r--r--spec/config/smime_signature_settings_spec.rb2
-rw-r--r--spec/contracts/provider/helpers/contract_source_helper.rb5
-rw-r--r--spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb8
-rw-r--r--spec/contracts/publish-contracts.sh3
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb2
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb100
-rw-r--r--spec/controllers/admin/cohorts_controller_spec.rb1
-rw-r--r--spec/controllers/admin/dev_ops_report_controller_spec.rb1
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb6
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb47
-rw-r--r--spec/controllers/admin/sessions_controller_spec.rb25
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb10
-rw-r--r--spec/controllers/admin/usage_trends_controller_spec.rb1
-rw-r--r--spec/controllers/admin/users_controller_spec.rb23
-rw-r--r--spec/controllers/application_controller_spec.rb23
-rw-r--r--spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb3
-rw-r--r--spec/controllers/concerns/confirm_email_warning_spec.rb2
-rw-r--r--spec/controllers/concerns/content_security_policy_patch_spec.rb2
-rw-r--r--spec/controllers/concerns/continue_params_spec.rb5
-rw-r--r--spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb11
-rw-r--r--spec/controllers/concerns/kas_cookie_spec.rb55
-rw-r--r--spec/controllers/concerns/product_analytics_tracking_spec.rb24
-rw-r--r--spec/controllers/concerns/renders_commits_spec.rb2
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb2
-rw-r--r--spec/controllers/concerns/sorting_preference_spec.rb41
-rw-r--r--spec/controllers/confirmations_controller_spec.rb74
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb46
-rw-r--r--spec/controllers/every_controller_spec.rb3
-rw-r--r--spec/controllers/explore/groups_controller_spec.rb4
-rw-r--r--spec/controllers/graphql_controller_spec.rb66
-rw-r--r--spec/controllers/groups/children_controller_spec.rb12
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb41
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb47
-rw-r--r--spec/controllers/groups/settings/applications_controller_spec.rb135
-rw-r--r--spec/controllers/groups/variables_controller_spec.rb10
-rw-r--r--spec/controllers/help_controller_spec.rb18
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb20
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb18
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb3
-rw-r--r--spec/controllers/import/github_controller_spec.rb63
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb54
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb3
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb75
-rw-r--r--spec/controllers/passwords_controller_spec.rb3
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb2
-rw-r--r--spec/controllers/profiles_controller_spec.rb32
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb34
-rw-r--r--spec/controllers/projects/badges_controller_spec.rb12
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb10
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb54
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb294
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb11
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb208
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb78
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb7
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb10
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb4
-rw-r--r--spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb11
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb131
-rw-r--r--spec/controllers/projects/feature_flags_controller_spec.rb11
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb15
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb7
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb16
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb1
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb2
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb12
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb53
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb21
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb20
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb80
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb43
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb19
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb16
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb117
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb57
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb121
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb6
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb103
-rw-r--r--spec/controllers/projects/prometheus/alerts_controller_spec.rb5
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb10
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb11
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb16
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb29
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb15
-rw-r--r--spec/controllers/projects/settings/merge_requests_controller_spec.rb11
-rw-r--r--spec/controllers/projects/snippets/blobs_controller_spec.rb17
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb11
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb47
-rw-r--r--spec/controllers/projects_controller_spec.rb142
-rw-r--r--spec/controllers/registrations/welcome_controller_spec.rb28
-rw-r--r--spec/controllers/registrations_controller_spec.rb219
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb67
-rw-r--r--spec/controllers/search_controller_spec.rb13
-rw-r--r--spec/controllers/sessions_controller_spec.rb48
-rw-r--r--spec/controllers/snippets/blobs_controller_spec.rb8
-rw-r--r--spec/db/schema_spec.rb32
-rw-r--r--spec/deprecation_warnings.rb21
-rw-r--r--spec/experiments/application_experiment_spec.rb8
-rw-r--r--spec/factories/abuse_reports.rb4
-rw-r--r--spec/factories/achievements/user_achievements.rb14
-rw-r--r--spec/factories/airflow/dags.rb8
-rw-r--r--spec/factories/bulk_import/batch_trackers.rb37
-rw-r--r--spec/factories/bulk_import/export_batches.rb23
-rw-r--r--spec/factories/chat_names.rb1
-rw-r--r--spec/factories/ci/builds.rb12
-rw-r--r--spec/factories/ci/catalog/resources.rb7
-rw-r--r--spec/factories/ci/runner_machine_builds.rb8
-rw-r--r--spec/factories/ci/runners.rb6
-rw-r--r--spec/factories/clusters/applications/helm.rb9
-rw-r--r--spec/factories/clusters/clusters.rb3
-rw-r--r--spec/factories/gitlab/database/async_foreign_keys/postgres_async_constraint_validation.rb17
-rw-r--r--spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb9
-rw-r--r--spec/factories/integrations.rb21
-rw-r--r--spec/factories/notes.rb5
-rw-r--r--spec/factories/packages/debian/component_file.rb7
-rw-r--r--spec/factories/packages/debian/file_metadatum.rb16
-rw-r--r--spec/factories/packages/package_files.rb29
-rw-r--r--spec/factories/packages/packages.rb1
-rw-r--r--spec/factories/project_error_tracking_settings.rb5
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/projects.rb27
-rw-r--r--spec/factories/serverless/domain.rb11
-rw-r--r--spec/factories/serverless/domain_cluster.rb17
-rw-r--r--spec/factories/service_desk/custom_email_verification.rb8
-rw-r--r--spec/factories/users.rb5
-rw-r--r--spec/factories/users/banned_users.rb7
-rw-r--r--spec/factories/work_items.rb7
-rw-r--r--spec/fast_spec_helper.rb26
-rw-r--r--spec/features/abuse_report_spec.rb19
-rw-r--r--spec/features/action_cable_logging_spec.rb2
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb223
-rw-r--r--spec/features/admin/admin_appearance_spec.rb2
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb2
-rw-r--r--spec/features/admin/admin_deploy_keys_spec.rb2
-rw-r--r--spec/features/admin/admin_health_check_spec.rb6
-rw-r--r--spec/features/admin/admin_mode/login_spec.rb22
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/workers_spec.rb2
-rw-r--r--spec/features/admin/admin_mode_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb25
-rw-r--r--spec/features/admin/admin_runners_spec.rb27
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb18
-rw-r--r--spec/features/admin/admin_system_info_spec.rb2
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb2
-rw-r--r--spec/features/admin_variables_spec.rb16
-rw-r--r--spec/features/boards/boards_spec.rb13
-rw-r--r--spec/features/boards/new_issue_spec.rb50
-rw-r--r--spec/features/breadcrumbs_schema_markup_spec.rb2
-rw-r--r--spec/features/calendar_spec.rb381
-rw-r--r--spec/features/callouts/registration_enabled_spec.rb2
-rw-r--r--spec/features/commit_spec.rb42
-rw-r--r--spec/features/contextual_sidebar_spec.rb69
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb10
-rw-r--r--spec/features/dashboard/groups_list_spec.rb7
-rw-r--r--spec/features/dashboard/projects_spec.rb24
-rw-r--r--spec/features/dashboard/root_explore_spec.rb6
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb2
-rw-r--r--spec/features/dashboard/snippets_spec.rb7
-rw-r--r--spec/features/display_system_header_and_footer_bar_spec.rb2
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb14
-rw-r--r--spec/features/explore/navbar_spec.rb13
-rw-r--r--spec/features/frequently_visited_projects_and_groups_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb15
-rw-r--r--spec/features/groups/board_spec.rb6
-rw-r--r--spec/features/groups/group_settings_spec.rb11
-rw-r--r--spec/features/groups/import_export/connect_instance_spec.rb2
-rw-r--r--spec/features/groups/members/sort_members_spec.rb2
-rw-r--r--spec/features/groups/new_group_page_spec.rb50
-rw-r--r--spec/features/groups/settings/access_tokens_spec.rb2
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb8
-rw-r--r--spec/features/help_dropdown_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/incidents/incident_timeline_events_spec.rb3
-rw-r--r--spec/features/incidents/user_views_alert_details_spec.rb34
-rw-r--r--spec/features/invites_spec.rb7
-rw-r--r--spec/features/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/issues/discussion_lock_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb66
-rw-r--r--spec/features/issues/issue_detail_spec.rb24
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb2
-rw-r--r--spec/features/issues/move_spec.rb3
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb20
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb22
-rw-r--r--spec/features/markdown/observability_spec.rb124
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_discussions_navigation_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_real_time_reviewers_spec.rb24
-rw-r--r--spec/features/merge_request/user_views_open_merge_request_spec.rb12
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb39
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb2
-rw-r--r--spec/features/nav/new_nav_toggle_spec.rb2
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb22
-rw-r--r--spec/features/nav/top_nav_spec.rb14
-rw-r--r--spec/features/populate_new_pipeline_vars_with_params_spec.rb2
-rw-r--r--spec/features/profiles/chat_names_spec.rb5
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb3
-rw-r--r--spec/features/profiles/user_creates_saved_reply_spec.rb29
-rw-r--r--spec/features/profiles/user_deletes_saved_reply_spec.rb27
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb20
-rw-r--r--spec/features/profiles/user_updates_saved_reply_spec.rb28
-rw-r--r--spec/features/profiles/user_uses_saved_reply_spec.rb29
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb2
-rw-r--r--spec/features/project_group_variables_spec.rb2
-rw-r--r--spec/features/project_variables_spec.rb15
-rw-r--r--spec/features/projects/badges/list_spec.rb39
-rw-r--r--spec/features/projects/blobs/blame_spec.rb47
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb8
-rw-r--r--spec/features/projects/branches_spec.rb24
-rw-r--r--spec/features/projects/ci/editor_spec.rb2
-rw-r--r--spec/features/projects/ci/lint_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb2
-rw-r--r--spec/features/projects/integrations/apple_app_store_spec.rb24
-rw-r--r--spec/features/projects/integrations/google_play_spec.rb25
-rw-r--r--spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_slack_notifications_spec.rb1
-rw-r--r--spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb13
-rw-r--r--spec/features/projects/members/sorting_spec.rb2
-rw-r--r--spec/features/projects/navbar_spec.rb1
-rw-r--r--spec/features/projects/new_project_spec.rb49
-rw-r--r--spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb12
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb44
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb6
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb2
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb9
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb9
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb19
-rw-r--r--spec/features/projects/work_items/work_item_children_spec.rb (renamed from spec/features/work_items/work_item_children_spec.rb)43
-rw-r--r--spec/features/projects/work_items/work_item_spec.rb58
-rw-r--r--spec/features/protected_tags_spec.rb34
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb18
-rw-r--r--spec/features/security/admin_access_spec.rb2
-rw-r--r--spec/features/security/dashboard_access_spec.rb2
-rw-r--r--spec/features/security/group/internal_access_spec.rb2
-rw-r--r--spec/features/security/group/private_access_spec.rb2
-rw-r--r--spec/features/security/group/public_access_spec.rb2
-rw-r--r--spec/features/security/project/internal_access_spec.rb2
-rw-r--r--spec/features/security/project/private_access_spec.rb2
-rw-r--r--spec/features/security/project/public_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/internal_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb2
-rw-r--r--spec/features/security/project/snippet/public_access_spec.rb2
-rw-r--r--spec/features/signed_commits_spec.rb2
-rw-r--r--spec/features/snippets/show_spec.rb5
-rw-r--r--spec/features/topic_show_spec.rb2
-rw-r--r--spec/features/u2f_spec.rb216
-rw-r--r--spec/features/unsubscribe_links_spec.rb2
-rw-r--r--spec/features/users/login_spec.rb204
-rw-r--r--spec/features/users/show_spec.rb34
-rw-r--r--spec/features/users/signup_spec.rb9
-rw-r--r--spec/features/webauthn_spec.rb247
-rw-r--r--spec/features/whats_new_spec.rb6
-rw-r--r--spec/features/work_items/work_item_spec.rb37
-rw-r--r--spec/finders/abuse_reports_finder_spec.rb111
-rw-r--r--spec/finders/group_members_finder_spec.rb52
-rw-r--r--spec/finders/merge_requests_finder_spec.rb18
-rw-r--r--spec/finders/milestones_finder_spec.rb92
-rw-r--r--spec/finders/serverless_domain_finder_spec.rb103
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json11
-rw-r--r--spec/fixtures/api/schemas/entities/discussion.json5
-rw-r--r--spec/fixtures/api/schemas/internal/pages/lookup_path.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/notes.json5
-rw-r--r--spec/fixtures/auth_key.p816
-rw-r--r--spec/fixtures/diagram.drawio.svg38
-rw-r--r--spec/fixtures/lib/gitlab/email/basic.html6
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json133
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson8
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/commit_notes.ndjson2
-rw-r--r--spec/fixtures/packages/debian/README.md2
-rw-r--r--spec/fixtures/packages/debian/sample-ddeb_1.2.3~alpha2_amd64.ddebbin0 -> 1068 bytes
-rw-r--r--spec/fixtures/packages/debian/sample/debian/.gitignore2
-rw-r--r--spec/fixtures/packages/debian/sample/debian/control4
-rwxr-xr-xspec/fixtures/packages/debian/sample/debian/rules7
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc9
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xzbin864 -> 964 bytes
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo306
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes21
-rw-r--r--spec/fixtures/service_account.json12
-rw-r--r--spec/fixtures/structure.sql8
-rw-r--r--spec/fixtures/work_items_invalid_types.csv4
-rw-r--r--spec/fixtures/work_items_missing_header.csv3
-rw-r--r--spec/fixtures/work_items_valid.csv3
-rw-r--r--spec/fixtures/work_items_valid_types.csv3
-rw-r--r--spec/frontend/.eslintrc.yml1
-rw-r--r--spec/frontend/__helpers__/create_mock_source_editor_extension.js12
-rw-r--r--spec/frontend/__helpers__/experimentation_helper.js7
-rw-r--r--spec/frontend/__helpers__/gon_helper.js5
-rw-r--r--spec/frontend/__helpers__/keep_alive_component_helper_spec.js4
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js16
-rw-r--r--spec/frontend/__helpers__/vue_mock_directive.js32
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper.js2
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper_spec.js14
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js18
-rw-r--r--spec/frontend/__mocks__/lodash/debounce.js19
-rw-r--r--spec/frontend/__mocks__/lodash/throttle.js2
-rw-r--r--spec/frontend/abuse_reports/components/abuse_category_selector_spec.js4
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js4
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js5
-rw-r--r--spec/frontend/access_tokens/components/token_spec.js4
-rw-r--r--spec/frontend/access_tokens/components/tokens_app_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js2
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js43
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js214
-rw-r--r--spec/frontend/admin/abuse_reports/components/app_spec.js104
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js14
-rw-r--r--spec/frontend/admin/abuse_reports/utils_spec.js13
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js4
-rw-r--r--spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js4
-rw-r--r--spec/frontend/admin/application_settings/network_outbound_spec.js70
-rw-r--r--spec/frontend/admin/applications/components/delete_application_spec.js1
-rw-r--r--spec/frontend/admin/background_migrations/components/database_listbox_spec.js4
-rw-r--r--spec/frontend/admin/broadcast_messages/components/base_spec.js5
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js4
-rw-r--r--spec/frontend/admin/broadcast_messages/components/messages_table_spec.js4
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js10
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js4
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js2
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js4
-rw-r--r--spec/frontend/admin/topics/components/remove_avatar_spec.js6
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js1
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js5
-rw-r--r--spec/frontend/admin/users/components/app_spec.js5
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js5
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js7
-rw-r--r--spec/frontend/admin/users/components/user_avatar_spec.js7
-rw-r--r--spec/frontend/admin/users/components/user_date_spec.js5
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js11
-rw-r--r--spec/frontend/admin/users/index_spec.js4
-rw-r--r--spec/frontend/airflow/dags/components/dags_spec.js115
-rw-r--r--spec/frontend/airflow/dags/components/mock_data.js67
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js2
-rw-r--r--spec/frontend/alert_spec.js (renamed from spec/frontend/flash_spec.js)2
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap33
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js12
-rw-r--r--spec/frontend/analytics/components/activity_chart_spec.js5
-rw-r--r--spec/frontend/analytics/cycle_analytics/base_spec.js33
-rw-r--r--spec/frontend/analytics/cycle_analytics/filter_bar_spec.js1
-rw-r--r--spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/path_navigation_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/stage_table_spec.js6
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/actions_spec.js31
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js13
-rw-r--r--spec/frontend/analytics/cycle_analytics/total_time_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/utils_spec.js23
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js5
-rw-r--r--spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js29
-rw-r--r--spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js4
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js15
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js4
-rw-r--r--spec/frontend/analytics/shared/components/metric_tile_spec.js4
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js4
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js28
-rw-r--r--spec/frontend/analytics/usage_trends/components/app_spec.js5
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_counts_spec.js4
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js5
-rw-r--r--spec/frontend/analytics/usage_trends/components/users_chart_spec.js5
-rw-r--r--spec/frontend/api/alert_management_alerts_api_spec.js3
-rw-r--r--spec/frontend/api/groups_api_spec.js13
-rw-r--r--spec/frontend/api/packages_api_spec.js12
-rw-r--r--spec/frontend/api/projects_api_spec.js3
-rw-r--r--spec/frontend/api/tags_api_spec.js3
-rw-r--r--spec/frontend/api/user_api_spec.js3
-rw-r--r--spec/frontend/api_spec.js25
-rw-r--r--spec/frontend/approvals/mock_data.js10
-rw-r--r--spec/frontend/artifacts/components/artifact_row_spec.js39
-rw-r--r--spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js96
-rw-r--r--spec/frontend/artifacts/components/artifacts_table_row_details_spec.js32
-rw-r--r--spec/frontend/artifacts/components/feedback_banner_spec.js4
-rw-r--r--spec/frontend/artifacts/components/job_artifacts_table_spec.js206
-rw-r--r--spec/frontend/artifacts/components/job_checkbox_spec.js71
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js2
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js104
-rw-r--r--spec/frontend/authentication/u2f/mock_u2f_device.js23
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js84
-rw-r--r--spec/frontend/authentication/u2f/util_spec.js61
-rw-r--r--spec/frontend/authentication/webauthn/components/registration_spec.js255
-rw-r--r--spec/frontend/authentication/webauthn/error_spec.js13
-rw-r--r--spec/frontend/authentication/webauthn/util_spec.js31
-rw-r--r--spec/frontend/awards_handler_spec.js5
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js1
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js1
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/diff_file_drafts_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/drafts_count_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js19
-rw-r--r--spec/frontend/batch_comments/components/preview_item_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.js1
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js6
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js10
-rw-r--r--spec/frontend/behaviors/components/diagram_performance_warning_spec.js4
-rw-r--r--spec/frontend/behaviors/components/json_table_spec.js4
-rw-r--r--spec/frontend/behaviors/copy_to_clipboard_spec.js2
-rw-r--r--spec/frontend/behaviors/markdown/highlight_current_user_spec.js10
-rw-r--r--spec/frontend/behaviors/markdown/render_observability_spec.js55
-rw-r--r--spec/frontend/blame/blame_redirect_spec.js4
-rw-r--r--spec/frontend/blame/streaming/index_spec.js110
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js170
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js53
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js1
-rw-r--r--spec/frontend/blob/csv/csv_viewer_spec.js4
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js2
-rw-r--r--spec/frontend/blob/pdf/pdf_viewer_spec.js5
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js1
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js5
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js4
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js2
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js8
-rw-r--r--spec/frontend/boards/board_list_helper.js1
-rw-r--r--spec/frontend/boards/board_list_spec.js4
-rw-r--r--spec/frontend/boards/components/board_add_new_column_form_spec.js124
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js61
-rw-r--r--spec/frontend/boards/components/board_add_new_column_trigger_spec.js6
-rw-r--r--spec/frontend/boards/components/board_app_spec.js3
-rw-r--r--spec/frontend/boards/components/board_card_spec.js2
-rw-r--r--spec/frontend/boards/components/board_column_spec.js6
-rw-r--r--spec/frontend/boards/components/board_configuration_options_spec.js4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js4
-rw-r--r--spec/frontend/boards/components/board_content_spec.js15
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js28
-rw-r--r--spec/frontend/boards/components/board_form_spec.js77
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js108
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js4
-rw-r--r--spec/frontend/boards/components/board_new_item_spec.js4
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js4
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js19
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js11
-rw-r--r--spec/frontend/boards/components/config_toggle_spec.js4
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js16
-rw-r--r--spec/frontend/boards/components/issue_due_date_spec.js4
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js4
-rw-r--r--spec/frontend/boards/components/item_count_spec.js8
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js5
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js5
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js2
-rw-r--r--spec/frontend/boards/components/toggle_focus_spec.js6
-rw-r--r--spec/frontend/boards/mock_data.js4
-rw-r--r--spec/frontend/boards/project_select_spec.js5
-rw-r--r--spec/frontend/boards/stores/actions_spec.js20
-rw-r--r--spec/frontend/boards/stores/getters_spec.js8
-rw-r--r--spec/frontend/branches/components/delete_branch_button_spec.js4
-rw-r--r--spec/frontend/branches/components/delete_branch_modal_spec.js4
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js6
-rw-r--r--spec/frontend/branches/components/divergence_graph_spec.js4
-rw-r--r--spec/frontend/branches/components/graph_bar_spec.js4
-rw-r--r--spec/frontend/captcha/captcha_modal_spec.js68
-rw-r--r--spec/frontend/ci/ci_lint/components/ci_lint_spec.js1
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js24
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js588
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js189
-rw-r--r--spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js76
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js102
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js39
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js61
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js112
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js87
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js17
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js17
-rw-r--r--spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js20
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js14
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js2
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js5
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/issue_status_icon_spec.js5
-rw-r--r--spec/frontend/ci/reports/components/report_link_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/report_section_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js5
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js93
-rw-r--r--spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js122
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap204
-rw-r--r--spec/frontend/ci/runner/components/registration/cli_command_spec.js39
-rw-r--r--spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js108
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_instructions_spec.js293
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/registration/utils_spec.js118
-rw-r--r--spec/frontend/ci/runner/components/runner_assigned_item_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js170
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_groups_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_membership_toggle_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_pagination_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/runner_paused_badge_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_projects_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_tag_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/runner_tags_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_type_tabs_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js4
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js4
-rw-r--r--spec/frontend/ci/runner/mock_data.js8
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js5
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap4
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/button_spec.js4
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/modal_spec.js1
-rw-r--r--spec/frontend/ci_secure_files/components/secure_files_list_spec.js13
-rw-r--r--spec/frontend/clusters/agents/components/activity_events_list_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/activity_history_item_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/create_token_button_spec.js6
-rw-r--r--spec/frontend/clusters/agents/components/create_token_modal_spec.js1
-rw-r--r--spec/frontend/clusters/agents/components/integration_status_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/revoke_token_button_spec.js3
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/token_table_spec.js4
-rw-r--r--spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap2
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap209
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js4
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js22
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js31
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/clusters_empty_state_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_main_view_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/clusters_view_all_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/delete_agent_button_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js1
-rw-r--r--spec/frontend/clusters_list/components/node_error_help_text_spec.js4
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js6
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js4
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js4
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js8
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js11
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js8
-rw-r--r--spec/frontend/commit/components/signature_badge_spec.js134
-rw-r--r--spec/frontend/commit/components/x509_certificate_details_spec.js36
-rw-r--r--spec/frontend/commit/mock_data.js31
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js118
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js1
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js4
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js4
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js4
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js4
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js97
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js4
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js68
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js4
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js14
-rw-r--r--spec/frontend/content_editor/components/loading_indicator_spec.js4
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js4
-rw-r--r--spec/frontend/content_editor/components/toolbar_image_button_spec.js1
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js1
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js40
-rw-r--r--spec/frontend/content_editor/components/toolbar_table_button_spec.js1
-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.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/details_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/label_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js8
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js4
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js4
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js21
-rw-r--r--spec/frontend/content_editor/extensions/drawio_diagram_spec.js103
-rw-r--r--spec/frontend/content_editor/extensions/paste_markdown_spec.js2
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js2
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js16
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js22
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js9
-rw-r--r--spec/frontend/content_editor/test_constants.js6
-rw-r--r--spec/frontend/content_editor/test_utils.js2
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap64
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js6
-rw-r--r--spec/frontend/contributors/store/actions_spec.js6
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js1
-rw-r--r--spec/frontend/crm/contacts_root_spec.js1
-rw-r--r--spec/frontend/crm/crm_form_spec.js4
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js4
-rw-r--r--spec/frontend/crm/organizations_root_spec.js1
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js1
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js5
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js5
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js5
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js1
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js5
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js5
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js51
-rw-r--r--spec/frontend/deploy_tokens/components/revoke_button_spec.js4
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js170
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js56
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js255
-rw-r--r--spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js5
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js10
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js2
-rw-r--r--spec/frontend/design_management/components/image_spec.js4
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js46
-rw-r--r--spec/frontend/design_management/components/upload/button_spec.js4
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js4
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js112
-rw-r--r--spec/frontend/design_management/mock_data/project.js17
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js81
-rw-r--r--spec/frontend/design_management/pages/index_spec.js15
-rw-r--r--spec/frontend/design_management/router_spec.js6
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js4
-rw-r--r--spec/frontend/diffs/components/app_spec.js79
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js5
-rw-r--r--spec/frontend/diffs/components/compare_dropdown_layout_spec.js5
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js5
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js5
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_file_row_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js13
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js4
-rw-r--r--spec/frontend/diffs/components/hidden_files_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js4
-rw-r--r--spec/frontend/diffs/components/merge_conflict_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js5
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js1
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js83
-rw-r--r--spec/frontend/diffs/store/actions_spec.js49
-rw-r--r--spec/frontend/diffs/store/getters_spec.js25
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js9
-rw-r--r--spec/frontend/diffs/store/utils_spec.js57
-rw-r--r--spec/frontend/diffs/utils/tree_worker_utils_spec.js30
-rw-r--r--spec/frontend/drawio/content_editor_facade_spec.js138
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js479
-rw-r--r--spec/frontend/drawio/markdown_field_editor_facade_spec.js148
-rw-r--r--spec/frontend/editor/components/helpers.js3
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js5
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_spec.js32
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js8
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml38
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml31
-rw-r--r--spec/frontend/editor/source_editor_ci_schema_ext_spec.js11
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js112
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js25
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js4
-rw-r--r--spec/frontend/editor/source_editor_webide_ext_spec.js1
-rw-r--r--spec/frontend/editor/utils_spec.js22
-rw-r--r--spec/frontend/emoji/components/category_spec.js21
-rw-r--r--spec/frontend/emoji/components/emoji_group_spec.js4
-rw-r--r--spec/frontend/emoji/components/emoji_list_spec.js33
-rw-r--r--spec/frontend/environment.js8
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js4
-rw-r--r--spec/frontend/environments/canary_update_modal_spec.js4
-rw-r--r--spec/frontend/environments/delete_environment_modal_spec.js6
-rw-r--r--spec/frontend/environments/edit_environment_spec.js5
-rw-r--r--spec/frontend/environments/empty_state_spec.js4
-rw-r--r--spec/frontend/environments/enable_review_app_modal_spec.js4
-rw-r--r--spec/frontend/environments/environment_actions_spec.js3
-rw-r--r--spec/frontend/environments/environment_folder_spec.js2
-rw-r--r--spec/frontend/environments/environment_form_spec.js20
-rw-r--r--spec/frontend/environments/environment_item_spec.js8
-rw-r--r--spec/frontend/environments/environment_pin_spec.js8
-rw-r--r--spec/frontend/environments/environment_table_spec.js4
-rw-r--r--spec/frontend/environments/environments_app_spec.js4
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js6
-rw-r--r--spec/frontend/environments/environments_folder_view_spec.js1
-rw-r--r--spec/frontend/environments/graphql/mock_data.js6
-rw-r--r--spec/frontend/environments/kubernetes_agent_info_spec.js126
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js84
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js83
-rw-r--r--spec/frontend/environments/new_environment_spec.js5
-rw-r--r--spec/frontend/environments/stop_stale_environments_modal_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js6
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js4
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js6
-rw-r--r--spec/frontend/experimentation/components/gitlab_experiment_spec.js7
-rw-r--r--spec/frontend/experimentation/utils_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js9
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js1
-rw-r--r--spec/frontend/feature_flags/components/empty_state_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js1
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategies/users_with_id_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/strategy_parameters_spec.js2
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js6
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_popover_spec.js5
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js5
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js4
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js4
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js4
-rw-r--r--spec/frontend/fixtures/abuse_reports.rb2
-rw-r--r--spec/frontend/fixtures/api_deploy_keys.rb5
-rw-r--r--spec/frontend/fixtures/jobs.rb22
-rw-r--r--spec/frontend/fixtures/merge_requests.rb6
-rw-r--r--spec/frontend/fixtures/runner.rb34
-rw-r--r--spec/frontend/fixtures/saved_replies.rb28
-rw-r--r--spec/frontend/fixtures/startup_css.rb20
-rw-r--r--spec/frontend/fixtures/u2f.rb48
-rw-r--r--spec/frontend/fixtures/users.rb65
-rw-r--r--spec/frontend/fixtures/webauthn.rb1
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js1
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js4
-rw-r--r--spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js4
-rw-r--r--spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js1
-rw-r--r--spec/frontend/google_cloud/components/google_cloud_menu_spec.js4
-rw-r--r--spec/frontend/google_cloud/components/incubation_banner_spec.js4
-rw-r--r--spec/frontend/google_cloud/components/revoke_oauth_spec.js4
-rw-r--r--spec/frontend/google_cloud/configuration/panel_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/panel_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/service_table_spec.js4
-rw-r--r--spec/frontend/google_cloud/deployments/panel_spec.js4
-rw-r--r--spec/frontend/google_cloud/deployments/service_table_spec.js4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/form_spec.js4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/list_spec.js4
-rw-r--r--spec/frontend/google_cloud/service_accounts/form_spec.js4
-rw-r--r--spec/frontend/google_cloud/service_accounts/list_spec.js4
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js6
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js3
-rw-r--r--spec/frontend/groups/components/app_spec.js15
-rw-r--r--spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js4
-rw-r--r--spec/frontend/groups/components/group_folder_spec.js4
-rw-r--r--spec/frontend/groups/components/group_item_spec.js4
-rw-r--r--spec/frontend/groups/components/group_name_and_path_spec.js4
-rw-r--r--spec/frontend/groups/components/groups_spec.js4
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js9
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js5
-rw-r--r--spec/frontend/groups/components/new_top_level_group_alert_spec.js4
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js1
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js4
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js4
-rw-r--r--spec/frontend/header_search/components/app_spec.js20
-rw-r--r--spec/frontend/header_search/components/header_search_autocomplete_items_spec.js11
-rw-r--r--spec/frontend/header_search/components/header_search_default_items_spec.js4
-rw-r--r--spec/frontend/header_search/components/header_search_scoped_items_spec.js7
-rw-r--r--spec/frontend/header_search/mock_data.js10
-rw-r--r--spec/frontend/header_search/store/actions_spec.js2
-rw-r--r--spec/frontend/header_search/store/getters_spec.js7
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js7
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js4
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js4
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js5
-rw-r--r--spec/frontend/ide/components/cannot_push_code_alert_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js39
-rw-r--r--spec/frontend/ide/components/commit_sidebar/empty_state_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/message_field_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/success_message_spec.js4
-rw-r--r--spec/frontend/ide/components/error_message_spec.js5
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js2
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js4
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_file_row_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_project_header_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js11
-rw-r--r--spec/frontend/ide/components/ide_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js3
-rw-r--r--spec/frontend/ide/components/ide_status_mr_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js5
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js5
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js5
-rw-r--r--spec/frontend/ide/components/nav_dropdown_button_spec.js4
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/button_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js21
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js4
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js5
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js5
-rw-r--r--spec/frontend/ide/components/pipelines/empty_state_spec.js4
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js5
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js5
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js5
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js4
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js5
-rw-r--r--spec/frontend/ide/components/shared/commit_message_field_spec.js4
-rw-r--r--spec/frontend/ide/components/shared/tokened_input_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/empty_state_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js4
-rw-r--r--spec/frontend/ide/services/index_spec.js3
-rw-r--r--spec/frontend/ide/services/terminals_spec.js2
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js4
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js6
-rw-r--r--spec/frontend/ide/stores/actions_spec.js8
-rw-r--r--spec/frontend/ide/stores/extend_spec.js5
-rw-r--r--spec/frontend/ide/stores/getters_spec.js7
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js76
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js8
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js6
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js4
-rw-r--r--spec/frontend/import_entities/components/import_status_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js43
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js56
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js5
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js6
-rw-r--r--spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js4
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js5
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js4
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js6
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/active_checkbox_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/confirmation_modal_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js51
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js57
-rw-r--r--spec/frontend/integrations/edit/components/sections/configuration_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/connection_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/google_play_spec.js54
-rw-r--r--spec/frontend/integrations/edit/components/sections/jira_issues_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/trigger_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/trigger_field_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js88
-rw-r--r--spec/frontend/integrations/index/components/integrations_list_spec.js4
-rw-r--r--spec/frontend/integrations/index/components/integrations_table_spec.js61
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js1
-rw-r--r--spec/frontend/integrations/overrides/components/integration_tabs_spec.js4
-rw-r--r--spec/frontend/invite_members/components/confetti_spec.js8
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js8
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js1
-rw-r--r--spec/frontend/invite_members/components/import_project_members_trigger_spec.js4
-rw-r--r--spec/frontend/invite_members/components/invite_group_trigger_spec.js5
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js17
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js30
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js49
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js31
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js5
-rw-r--r--spec/frontend/invite_members/components/project_select_spec.js4
-rw-r--r--spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js4
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js18
-rw-r--r--spec/frontend/issuable/components/csv_import_export_buttons_spec.js6
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js4
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js2
-rw-r--r--spec/frontend/issuable/components/issuable_header_warnings_spec.js7
-rw-r--r--spec/frontend/issuable/components/issue_assignees_spec.js5
-rw-r--r--spec/frontend/issuable/components/issue_milestone_spec.js159
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js4
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js5
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js4
-rw-r--r--spec/frontend/issuable/popover/components/mr_popover_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js243
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js14
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js5
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js8
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js20
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js4
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js80
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js5
-rw-r--r--spec/frontend/issues/list/mock_data.js20
-rw-r--r--spec/frontend/issues/list/utils_spec.js5
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_item_spec.js4
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js123
-rw-r--r--spec/frontend/issues/new/components/type_popover_spec.js4
-rw-r--r--spec/frontend/issues/new/mock_data.js64
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js1
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js4
-rw-r--r--spec/frontend/issues/show/components/app_spec.js579
-rw-r--r--spec/frontend/issues/show/components/delete_issue_modal_spec.js4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js182
-rw-r--r--spec/frontend/issues/show/components/edit_actions_spec.js4
-rw-r--r--spec/frontend/issues/show/components/edited_spec.js4
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js28
-rw-r--r--spec/frontend/issues/show/components/fields/description_template_spec.js4
-rw-r--r--spec/frontend/issues/show/components/fields/title_spec.js5
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js8
-rw-r--r--spec/frontend/issues/show/components/form_spec.js4
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js47
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js5
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js55
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js5
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js8
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js4
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js4
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js5
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js2
-rw-r--r--spec/frontend/issues/show/components/title_spec.js89
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js57
-rw-r--r--spec/frontend/jira_connect/branches/components/new_branch_form_spec.js4
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js4
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js4
-rw-r--r--spec/frontend/jira_connect/branches/pages/index_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js3
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js6
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/user_link_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js4
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap6
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js5
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js1
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js5
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js5
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js4
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/artifacts_block_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/commit_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/environments_block_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/erased_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js3
-rw-r--r--spec/frontend/jobs/components/job/job_container_item_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/job_log_controllers_spec.js12
-rw-r--r--spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js12
-rw-r--r--spec/frontend/jobs/components/job/jobs_container_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js106
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js27
-rw-r--r--spec/frontend/jobs/components/job/sidebar_detail_row_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/stages_dropdown_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/trigger_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/duration_badge_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_number_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js15
-rw-r--r--spec/frontend/jobs/components/table/cells/duration_cell_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/cells/job_cell_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/graphql/cache_config_spec.js19
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js53
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_tabs_spec.js4
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js5
-rw-r--r--spec/frontend/jobs/mock_data.js2
-rw-r--r--spec/frontend/labels/components/delete_label_modal_spec.js4
-rw-r--r--spec/frontend/labels/components/promote_label_modal_spec.js1
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js4
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js5
-rw-r--r--spec/frontend/lib/dompurify_spec.js14
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js7
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js8
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js1
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js4
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js10
-rw-r--r--spec/frontend/lib/utils/error_message_spec.js65
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js22
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js29
-rw-r--r--spec/frontend/lib/utils/ref_validator_spec.js79
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js62
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js32
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js8
-rw-r--r--spec/frontend/lib/utils/vuex_module_mappers_spec.js4
-rw-r--r--spec/frontend/locale/sprintf_spec.js11
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js6
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js7
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js6
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js6
-rw-r--r--spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js6
-rw-r--r--spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js4
-rw-r--r--spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js6
-rw-r--r--spec/frontend/members/components/app_spec.js1
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/avatars/invite_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js2
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js4
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js4
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js5
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js4
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js4
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js6
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js5
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js4
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js10
-rw-r--r--spec/frontend/members/index_spec.js3
-rw-r--r--spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js4
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js6
-rw-r--r--spec/frontend/merge_request_spec.js4
-rw-r--r--spec/frontend/merge_request_tabs_spec.js2
-rw-r--r--spec/frontend/merge_requests/components/compare_app_spec.js4
-rw-r--r--spec/frontend/merge_requests/components/compare_dropdown_spec.js1
-rw-r--r--spec/frontend/milestones/components/delete_milestone_modal_spec.js8
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js5
-rw-r--r--spec/frontend/milestones/components/promote_milestone_modal_spec.js8
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap (renamed from spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap)21
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js)8
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js)105
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js52
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js5
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js4
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js5
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js16
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js5
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js4
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js4
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js4
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js1
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js6
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js6
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js4
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js11
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js186
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js4
-rw-r--r--spec/frontend/nav/components/responsive_header_spec.js6
-rw-r--r--spec/frontend/nav/components/responsive_home_spec.js6
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js29
-rw-r--r--spec/frontend/notebook/cells/code_spec.js4
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js6
-rw-r--r--spec/frontend/notebook/cells/output/error_spec.js48
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js4
-rw-r--r--spec/frontend/notebook/cells/prompt_spec.js4
-rw-r--r--spec/frontend/notebook/index_spec.js6
-rw-r--r--spec/frontend/notebook/mock_data.js6
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js120
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js4
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js11
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js3
-rw-r--r--spec/frontend/notes/components/discussion_filter_note_spec.js5
-rw-r--r--spec/frontend/notes/components/discussion_navigator_spec.js1
-rw-r--r--spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js5
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js4
-rw-r--r--spec/frontend/notes/components/email_participants_warning_spec.js5
-rw-r--r--spec/frontend/notes/components/note_actions/reply_button_spec.js5
-rw-r--r--spec/frontend/notes/components/note_actions/timeline_event_button_spec.js4
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js7
-rw-r--r--spec/frontend/notes/components/note_attachment_spec.js5
-rw-r--r--spec/frontend/notes/components/note_body_spec.js4
-rw-r--r--spec/frontend/notes/components/note_edited_text_spec.js69
-rw-r--r--spec/frontend/notes/components/note_form_spec.js4
-rw-r--r--spec/frontend/notes/components/note_header_spec.js5
-rw-r--r--spec/frontend/notes/components/note_signed_out_widget_spec.js4
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js15
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js4
-rw-r--r--spec/frontend/notes/components/notes_activity_header_spec.js4
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js3
-rw-r--r--spec/frontend/notes/components/toggle_replies_widget_spec.js4
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js1
-rw-r--r--spec/frontend/notes/stores/actions_spec.js26
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js52
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js4
-rw-r--r--spec/frontend/observability/index_spec.js64
-rw-r--r--spec/frontend/observability/observability_app_spec.js144
-rw-r--r--spec/frontend/observability/skeleton_spec.js15
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js7
-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.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js43
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js26
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js3
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js13
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js4
-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/details_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap3
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js80
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js17
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js34
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js1
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js25
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js1
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js1
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_path_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/publish_method_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_list_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/shared/components/settings_block_spec.js4
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js4
-rw-r--r--spec/frontend/pages/groups/new/components/app_spec.js25
-rw-r--r--spec/frontend/pages/groups/new/components/create_group_description_details_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.js11
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js11
-rw-r--r--spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js7
-rw-r--r--spec/frontend/pages/projects/forks/new/components/app_spec.js4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js7
-rw-r--r--spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js17
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js11
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js6
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js8
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js5
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js4
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js5
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js5
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js5
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js9
-rw-r--r--spec/frontend/pdf/index_spec.js4
-rw-r--r--spec/frontend/pdf/page_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/add_request_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/request_warning_spec.js4
-rw-r--r--spec/frontend/persistent_user_callout_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js14
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/input_wrapper_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/step_nav_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/list_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js21
-rw-r--r--spec/frontend/pipeline_wizard/pipeline_wizard_spec.js4
-rw-r--r--spec/frontend/pipelines/components/dag/dag_annotations_spec.js5
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js5
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js5
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js8
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js8
-rw-r--r--spec/frontend/pipelines/components/jobs/jobs_app_spec.js8
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js12
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js15
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js4
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js5
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js5
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/job_group_dropdown_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js47
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js1
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js4
-rw-r--r--spec/frontend/pipelines/header_component_spec.js5
-rw-r--r--spec/frontend/pipelines/nav_controls_spec.js37
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js4
-rw-r--r--spec/frontend/pipelines/pipeline_labels_spec.js4
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js6
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js4
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js7
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js5
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js5
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js5
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js4
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js5
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js5
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js6
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js46
-rw-r--r--spec/frontend/profile/components/activity_calendar_spec.js120
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/overview_tab_spec.js7
-rw-r--r--spec/frontend/profile/components/user_achievements_spec.js102
-rw-r--r--spec/frontend/profile/mock_data.js22
-rw-r--r--spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js5
-rw-r--r--spec/frontend/profile/preferences/components/diffs_colors_spec.js5
-rw-r--r--spec/frontend/profile/preferences/components/integration_view_spec.js5
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js9
-rw-r--r--spec/frontend/profile/utils_spec.js15
-rw-r--r--spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js4
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js4
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js3
-rw-r--r--spec/frontend/projects/commit/components/projects_dropdown_spec.js15
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js6
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js1
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js8
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/repo_dropdown_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/revision_card_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js51
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js48
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js5
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js5
-rw-r--r--spec/frontend/projects/details/upload_button_spec.js4
-rw-r--r--spec/frontend/projects/new/components/app_spec.js29
-rw-r--r--spec/frontend/projects/new/components/deployment_target_select_spec.js1
-rw-r--r--spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js1
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js21
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap29
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js4
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js5
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js5
-rw-r--r--spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js4
-rw-r--r--spec/frontend/projects/prune_unreachable_objects_button_spec.js7
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js8
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js48
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js2
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js2
-rw-r--r--spec/frontend/projects/settings/components/default_branch_selector_spec.js4
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js20
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js4
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js6
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js2
-rw-r--r--spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js1
-rw-r--r--spec/frontend/projects/settings/utils_spec.js24
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js1
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js4
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js6
-rw-r--r--spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap80
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js53
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js7
-rw-r--r--spec/frontend/releases/components/app_index_spec.js12
-rw-r--r--spec/frontend/releases/components/app_show_spec.js15
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js5
-rw-r--r--spec/frontend/releases/components/confirm_delete_modal_spec.js4
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js4
-rw-r--r--spec/frontend/releases/components/issuable_stats_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js4
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js5
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js8
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js5
-rw-r--r--spec/frontend/releases/release_notification_service_spec.js8
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js20
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js81
-rw-r--r--spec/frontend/repository/commits_service_spec.js4
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js4
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js6
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js2
-rw-r--r--spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js2
-rw-r--r--spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js22
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js4
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js8
-rw-r--r--spec/frontend/repository/components/directory_download_links_spec.js4
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js137
-rw-r--r--spec/frontend/repository/components/fork_suggestion_spec.js2
-rw-r--r--spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js42
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js33
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js12
-rw-r--r--spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap42
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js93
-rw-r--r--spec/frontend/repository/components/table/index_spec.js4
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js4
-rw-r--r--spec/frontend/repository/components/table/row_spec.js6
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js50
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js52
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js2
-rw-r--r--spec/frontend/repository/mock_data.js6
-rw-r--r--spec/frontend/repository/pages/blob_spec.js4
-rw-r--r--spec/frontend/repository/pages/index_spec.js2
-rw-r--r--spec/frontend/repository/pages/tree_spec.js2
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap48
-rw-r--r--spec/frontend/saved_replies/components/form_spec.js144
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js36
-rw-r--r--spec/frontend/saved_replies/components/list_spec.js48
-rw-r--r--spec/frontend/saved_replies/pages/index_spec.js45
-rw-r--r--spec/frontend/search/mock_data.js7
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js34
-rw-r--r--spec/frontend/search/sidebar/components/checkbox_filter_spec.js9
-rw-r--r--spec/frontend/search/sidebar/components/filters_spec.js21
-rw-r--r--spec/frontend/search/sidebar/components/language_filter_spec.js (renamed from spec/frontend/search/sidebar/components/language_filters_spec.js)27
-rw-r--r--spec/frontend/search/sidebar/components/radio_filter_spec.js10
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js39
-rw-r--r--spec/frontend/search/sort/components/app_spec.js5
-rw-r--r--spec/frontend/search/store/actions_spec.js48
-rw-r--r--spec/frontend/search/store/getters_spec.js47
-rw-r--r--spec/frontend/search/store/utils_spec.js35
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js49
-rw-r--r--spec/frontend/search_autocomplete_spec.js1
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js30
-rw-r--r--spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js6
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js3
-rw-r--r--spec/frontend/security_configuration/components/upgrade_banner_spec.js1
-rw-r--r--spec/frontend/security_configuration/constants.js1
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap35
-rw-r--r--spec/frontend/sentry/index_spec.js7
-rw-r--r--spec/frontend/sentry/legacy_index_spec.js7
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js7
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js7
-rw-r--r--spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js7
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_title_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js1
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js3
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js11
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js10
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js4
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js4
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js12
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/copy/copyable_field_spec.js4
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js8
-rw-r--r--spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js5
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js15
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js4
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js4
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_status_spec.js4
-rw-r--r--spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js16
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js10
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js20
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js13
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_spec.js5
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js7
-rw-r--r--spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js1
-rw-r--r--spec/frontend/sidebar/components/move/move_issue_button_spec.js6
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js21
-rw-r--r--spec/frontend/sidebar/components/participants/participants_spec.js197
-rw-r--r--spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js1
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js5
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewers_spec.js4
-rw-r--r--spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js3
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js13
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js11
-rw-r--r--spec/frontend/sidebar/components/status/status_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js5
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js4
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js5
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js6
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js1
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_spec.js4
-rw-r--r--spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js4
-rw-r--r--spec/frontend/sidebar/mock_data.js1
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap3
-rw-r--r--spec/frontend/snippets/components/edit_spec.js21
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js5
-rw-r--r--spec/frontend/snippets/components/show_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js5
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_description_edit_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_description_view_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js136
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js4
-rw-r--r--spec/frontend/snippets/mock_data.js19
-rw-r--r--spec/frontend/streaming/chunk_writer_spec.js214
-rw-r--r--spec/frontend/streaming/handle_streamed_anchor_link_spec.js132
-rw-r--r--spec/frontend/streaming/html_stream_spec.js46
-rw-r--r--spec/frontend/streaming/rate_limit_stream_requests_spec.js155
-rw-r--r--spec/frontend/streaming/render_balancer_spec.js69
-rw-r--r--spec/frontend/streaming/render_html_streams_spec.js96
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js219
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js50
-rw-r--r--spec/frontend/super_sidebar/components/frequent_items_list_spec.js68
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js238
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js102
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js120
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js516
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js404
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/actions_spec.js113
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/getters_spec.js333
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js87
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js11
-rw-r--r--spec/frontend/super_sidebar/components/items_list_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js49
-rw-r--r--spec/frontend/super_sidebar/components/projects_list_spec.js82
-rw-r--r--spec/frontend/super_sidebar/components/search_results_spec.js57
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_portal_spec.js68
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js27
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js31
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js375
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js100
-rw-r--r--spec/frontend/super_sidebar/mock_data.js187
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js157
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js160
-rw-r--r--spec/frontend/syntax_highlight_spec.js6
-rw-r--r--spec/frontend/tags/components/delete_tag_modal_spec.js4
-rw-r--r--spec/frontend/terms/components/app_spec.js4
-rw-r--r--spec/frontend/terraform/components/empty_state_spec.js4
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js4
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js8
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js7
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js5
-rw-r--r--spec/frontend/toggles/index_spec.js1
-rw-r--r--spec/frontend/token_access/inbound_token_access_spec.js4
-rw-r--r--spec/frontend/token_access/opt_in_jwt_spec.js4
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js4
-rw-r--r--spec/frontend/token_access/token_access_app_spec.js14
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js38
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js5
-rw-r--r--spec/frontend/usage_quotas/components/usage_quotas_app_spec.js4
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js4
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js3
-rw-r--r--spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js4
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js4
-rw-r--r--spec/frontend/user_lists/components/user_lists_spec.js5
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js4
-rw-r--r--spec/frontend/user_popovers_spec.js11
-rw-r--r--spec/frontend/validators/length_validator_spec.js91
-rw-r--r--spec/frontend/vue_compat_test_setup.js60
-rw-r--r--spec/frontend/vue_merge_request_widget/components/action_buttons.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js221
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js21
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js78
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js62
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js136
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js2
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap40
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/alert_details_table_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/chronic_duration_input_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/confirm_fork_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/content_transition_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/dom_element_listener_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ensure_data_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/expand_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_row_header_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/file_tree_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js86
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js139
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js126
-rw-r--r--spec/frontend/vue_shared/components/form/form_footer_actions_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/form/title_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/integration_help_text_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/keep_alive_slots_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js66
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js193
-rw-r--r--spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/memory_graph_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ordered_layout_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/page_size_selector_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/pagination_links_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/panel_resizer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/papa_parse_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/project_avatar_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js136
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/details_row_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/history_item_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap23
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/security_reports/help_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/security_reports/security_summary_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/segmented_control_button_group_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/smart_virtual_list_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/source_editor_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_callout_dismisser_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/vuex_module_provider_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/directives/validation_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js15
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js20
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js6
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js3
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js28
-rw-r--r--spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js1
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js2
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/welcome_spec.js2
-rw-r--r--spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js9
-rw-r--r--spec/frontend/vue_shared/plugins/global_toast_spec.js22
-rw-r--r--spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js4
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js4
-rw-r--r--spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js5
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js8
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js4
-rw-r--r--spec/frontend/whats_new/components/app_spec.js3
-rw-r--r--spec/frontend/whats_new/components/feature_spec.js5
-rw-r--r--spec/frontend/whats_new/utils/get_drawer_body_height_spec.js4
-rw-r--r--spec/frontend/work_items/components/app_spec.js4
-rw-r--r--spec/frontend/work_items/components/item_state_spec.js4
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js4
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap2
-rw-r--r--spec/frontend/work_items/components/notes/activity_filter_spec.js74
-rw-r--r--spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js109
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js26
-rw-r--r--spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js44
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js98
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js12
-rw-r--r--spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js63
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js9
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js49
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js34
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js13
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js71
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js6
-rw-r--r--spec/frontend/work_items/mock_data.js236
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js24
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js4
-rw-r--r--spec/frontend/work_items/router_spec.js2
-rw-r--r--spec/frontend/work_items_hierarchy/components/app_spec.js4
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js4
-rw-r--r--spec/frontend/zen_mode_spec.js29
-rw-r--r--spec/frontend_integration/content_editor/content_editor_integration_spec.js4
-rw-r--r--spec/frontend_integration/ide/ide_integration_spec.js1
-rw-r--r--spec/frontend_integration/ide/user_opens_file_spec.js1
-rw-r--r--spec/frontend_integration/ide/user_opens_ide_spec.js1
-rw-r--r--spec/frontend_integration/ide/user_opens_mr_spec.js1
-rw-r--r--spec/graphql/mutations/achievements/award_spec.rb53
-rw-r--r--spec/graphql/mutations/achievements/revoke_spec.rb57
-rw-r--r--spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb1
-rw-r--r--spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb1
-rw-r--r--spec/graphql/mutations/alert_management/create_alert_issue_spec.rb2
-rw-r--r--spec/graphql/mutations/alert_management/update_alert_status_spec.rb1
-rw-r--r--spec/graphql/mutations/members/bulk_update_base_spec.rb16
-rw-r--r--spec/graphql/mutations/release_asset_links/create_spec.rb2
-rw-r--r--spec/graphql/mutations/release_asset_links/delete_spec.rb14
-rw-r--r--spec/graphql/mutations/release_asset_links/update_spec.rb2
-rw-r--r--spec/graphql/resolvers/achievements/achievements_resolver_spec.rb34
-rw-r--r--spec/graphql/resolvers/ci/group_runners_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/project_runners_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb10
-rw-r--r--spec/graphql/resolvers/ci/variables_resolver_spec.rb2
-rw-r--r--spec/graphql/types/achievements/achievement_type_spec.rb1
-rw-r--r--spec/graphql/types/achievements/user_achievement_type_spec.rb24
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb6
-rw-r--r--spec/graphql/types/ci/runner_machine_type_spec.rb18
-rw-r--r--spec/graphql/types/ci/runner_type_spec.rb8
-rw-r--r--spec/graphql/types/ci/variable_sort_enum_spec.rb2
-rw-r--r--spec/graphql/types/commit_signature_interface_spec.rb5
-rw-r--r--spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb2
-rw-r--r--spec/graphql/types/design_management/design_at_version_type_spec.rb2
-rw-r--r--spec/graphql/types/design_management/design_type_spec.rb7
-rw-r--r--spec/graphql/types/key_type_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/graphql/types/projects/fork_details_type_spec.rb2
-rw-r--r--spec/graphql/types/root_storage_statistics_type_spec.rb2
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/graphql/types/work_items/available_export_fields_enum_spec.rb26
-rw-r--r--spec/graphql/types/work_items/widget_interface_spec.rb11
-rw-r--r--spec/graphql/types/work_items/widgets/notifications_type_spec.rb12
-rw-r--r--spec/graphql/types/work_items/widgets/notifications_update_input_type_spec.rb9
-rw-r--r--spec/helpers/admin/abuse_reports_helper_spec.rb24
-rw-r--r--spec/helpers/analytics/cycle_analytics_helper_spec.rb61
-rw-r--r--spec/helpers/application_settings_helper_spec.rb6
-rw-r--r--spec/helpers/artifacts_helper_spec.rb1
-rw-r--r--spec/helpers/ci/catalog/resources_helper_spec.rb33
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb2
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb6
-rw-r--r--spec/helpers/ci/variables_helper_spec.rb2
-rw-r--r--spec/helpers/commits_helper_spec.rb36
-rw-r--r--spec/helpers/device_registration_helper_spec.rb37
-rw-r--r--spec/helpers/diff_helper_spec.rb59
-rw-r--r--spec/helpers/explore_helper_spec.rb2
-rw-r--r--spec/helpers/groups/observability_helper_spec.rb91
-rw-r--r--spec/helpers/groups_helper_spec.rb6
-rw-r--r--spec/helpers/ide_helper_spec.rb190
-rw-r--r--spec/helpers/issuables_helper_spec.rb25
-rw-r--r--spec/helpers/jira_connect_helper_spec.rb19
-rw-r--r--spec/helpers/markup_helper_spec.rb39
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb71
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb52
-rw-r--r--spec/helpers/notes_helper_spec.rb13
-rw-r--r--spec/helpers/packages_helper_spec.rb15
-rw-r--r--spec/helpers/plan_limits_helper_spec.rb29
-rw-r--r--spec/helpers/projects/settings/branch_rules_helper_spec.rb25
-rw-r--r--spec/helpers/projects_helper_spec.rb31
-rw-r--r--spec/helpers/registrations_helper_spec.rb16
-rw-r--r--spec/helpers/search_helper_spec.rb15
-rw-r--r--spec/helpers/sidebars_helper_spec.rb158
-rw-r--r--spec/helpers/sorting_helper_spec.rb54
-rw-r--r--spec/helpers/todos_helper_spec.rb12
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb70
-rw-r--r--spec/helpers/users_helper_spec.rb58
-rw-r--r--spec/initializers/check_forced_decomposition_spec.rb6
-rw-r--r--spec/initializers/direct_upload_support_spec.rb4
-rw-r--r--spec/initializers/google_cloud_profiler_spec.rb87
-rw-r--r--spec/initializers/safe_session_store_patch_spec.rb62
-rw-r--r--spec/lib/api/entities/ssh_key_spec.rb2
-rw-r--r--spec/lib/api/helpers/internal_helpers_spec.rb60
-rw-r--r--spec/lib/api/helpers/packages_helpers_spec.rb3
-rw-r--r--spec/lib/api/helpers_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb21
-rw-r--r--spec/lib/atlassian/jira_connect_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_issue_key_extractor_spec.rb2
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb11
-rw-r--r--spec/lib/banzai/filter/inline_observability_filter_spec.rb82
-rw-r--r--spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb123
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb9
-rw-r--r--spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb9
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb3
-rw-r--r--spec/lib/bulk_imports/clients/http_spec.rb33
-rw-r--r--spec/lib/bulk_imports/features_spec.rb43
-rw-r--r--spec/lib/bulk_imports/groups/stage_spec.rb58
-rw-r--r--spec/lib/bulk_imports/ndjson_pipeline_spec.rb54
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb8
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/commit_notes_pipeline_spec.rb69
-rw-r--r--spec/lib/container_registry/gitlab_api_client_spec.rb202
-rw-r--r--spec/lib/error_tracking/collector/payload_validator_spec.rb2
-rw-r--r--spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb82
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/my_batched_migration.txt22
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt6
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_spec_matcher.txt7
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt28
-rw-r--r--spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration_spec.txt26
-rw-r--r--spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb26
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb4
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb26
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb4
-rw-r--r--spec/lib/gitlab/api_authentication/token_resolver_spec.rb2
-rw-r--r--spec/lib/gitlab/app_logger_spec.rb25
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb2
-rw-r--r--spec/lib/gitlab/audit/auditor_spec.rb33
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/duo_auth/manual_otp_spec.rb94
-rw-r--r--spec/lib/gitlab/auth_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_prepared_at_merge_requests_spec.rb57
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_wiki_repositories_spec.rb65
-rw-r--r--spec/lib/gitlab/background_migration/batched_migration_job_spec.rb22
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphaned_packages_dependencies_spec.rb57
-rw-r--r--spec/lib/gitlab/background_migration/fix_vulnerability_reads_has_issues_spec.rb100
-rw-r--r--spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb90
-rw-r--r--spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb136
-rw-r--r--spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb141
-rw-r--r--spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb173
-rw-r--r--spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb3
-rw-r--r--spec/lib/gitlab/background_task_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/client_spec.rb165
-rw-r--r--spec/lib/gitlab/changes_list_spec.rb2
-rw-r--r--spec/lib/gitlab/chat/responder_spec.rb68
-rw-r--r--spec/lib/gitlab/checks/changes_access_spec.rb12
-rw-r--r--spec/lib/gitlab/checks/diff_check_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/badge/release/template_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/hook_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/components/header_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/context_spec.rb68
-rw-r--r--spec/lib/gitlab/ci/config/external/file/artifact_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/file/component_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/file/local_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/file/template_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb288
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/rules_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/header/input_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/header/root_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/config/header/spec_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/config/yaml/result_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/config/yaml_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/input/arguments/base_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/input/arguments/default_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/input/arguments/options_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/input/arguments/required_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/input/arguments/unknown_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/input/inputs_spec.rb126
-rw-r--r--spec/lib/gitlab/ci/interpolation/access_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/block_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/context_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/template_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/pipeline/duration_spec.rb156
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/project_config/repository_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/project_config/source_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb163
-rw-r--r--spec/lib/gitlab/ci/runner_releases_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/result_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb2
-rw-r--r--spec/lib/gitlab/color_schemes_spec.rb8
-rw-r--r--spec/lib/gitlab/config/entry/validators_spec.rb2
-rw-r--r--spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb198
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb28
-rw-r--r--spec/lib/gitlab/console_spec.rb4
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb53
-rw-r--r--spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb288
-rw-r--r--spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb109
-rw-r--r--spec/lib/gitlab/database/async_constraints/validators/check_constraint_spec.rb20
-rw-r--r--spec/lib/gitlab/database/async_constraints/validators/foreign_key_spec.rb35
-rw-r--r--spec/lib/gitlab/database/async_constraints/validators_spec.rb21
-rw-r--r--spec/lib/gitlab/database/async_constraints_spec.rb29
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb152
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb167
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb52
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys_spec.rb23
-rw-r--r--spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb9
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb165
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb15
-rw-r--r--spec/lib/gitlab/database/gitlab_schema_spec.rb20
-rw-r--r--spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb35
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb110
-rw-r--r--spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb44
-rw-r--r--spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb35
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb21
-rw-r--r--spec/lib/gitlab/database/partitioning/ci_sliding_list_strategy_spec.rb178
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb27
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb122
-rw-r--r--spec/lib/gitlab/database/partitioning_spec.rb11
-rw-r--r--spec/lib/gitlab/database/postgres_foreign_key_spec.rb24
-rw-r--r--spec/lib/gitlab/database/postgres_partition_spec.rb14
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb4
-rw-r--r--spec/lib/gitlab/database/schema_validation/database_spec.rb111
-rw-r--r--spec/lib/gitlab/database/schema_validation/index_spec.rb22
-rw-r--r--spec/lib/gitlab/database/schema_validation/indexes_spec.rb56
-rw-r--r--spec/lib/gitlab/database/schema_validation/runner_spec.rb50
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb10
-rw-r--r--spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb10
-rw-r--r--spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb82
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb31
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/different_definition_indexes_spec.rb8
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb8
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/extra_indexes_spec.rb7
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb7
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/missing_indexes_spec.rb14
-rw-r--r--spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb9
-rw-r--r--spec/lib/gitlab/database/tables_locker_spec.rb219
-rw-r--r--spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb11
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/html_to_markdown_parser_spec.rb12
-rw-r--r--spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb6
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb6
-rw-r--r--spec/lib/gitlab/endpoint_attributes_spec.rb7
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb7
-rw-r--r--spec/lib/gitlab/exception_log_formatter_spec.rb6
-rw-r--r--spec/lib/gitlab/external_authorization/config_spec.rb2
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb126
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb3
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb20
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb54
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb20
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb41
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/clients/proxy_spec.rb123
-rw-r--r--spec/lib/gitlab/github_import/importer/collaborator_importer_spec.rb86
-rw-r--r--spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb119
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb18
-rw-r--r--spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb45
-rw-r--r--spec/lib/gitlab/github_import/markdown/attachment_spec.rb58
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb89
-rw-r--r--spec/lib/gitlab/github_import/project_relation_type_spec.rb49
-rw-r--r--spec/lib/gitlab/github_import/representation/collaborator_spec.rb39
-rw-r--r--spec/lib/gitlab/i18n/pluralization_spec.rb53
-rw-r--r--spec/lib/gitlab/i18n_spec.rb17
-rw-r--r--spec/lib/gitlab/import/errors_spec.rb48
-rw-r--r--spec/lib/gitlab/import/metrics_spec.rb130
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml110
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attributes_finder_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/attributes_permitter_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/base/relation_object_saver_spec.rb30
-rw-r--r--spec/lib/gitlab/import_export/command_line_util_spec.rb61
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb79
-rw-r--r--spec/lib/gitlab/import_export/import_failure_service_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_writer_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/import_task_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/object_builder_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb19
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/references_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml107
-rw-r--r--spec/lib/gitlab/instrumentation/redis_base_spec.rb12
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb34
-rw-r--r--spec/lib/gitlab/internal_post_receive/response_spec.rb2
-rw-r--r--spec/lib/gitlab/issuable_sorter_spec.rb42
-rw-r--r--spec/lib/gitlab/jira_import/issues_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/kas/user_access_spec.rb96
-rw-r--r--spec/lib/gitlab/kroki_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/config_map_spec.rb16
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb31
-rw-r--r--spec/lib/gitlab/loggable_spec.rb68
-rw-r--r--spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb3
-rw-r--r--spec/lib/gitlab/metrics/boot_time_tracker_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb27
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb31
-rw-r--r--spec/lib/gitlab/monitor/demo_projects_spec.rb6
-rw-r--r--spec/lib/gitlab/multi_collection_paginator_spec.rb7
-rw-r--r--spec/lib/gitlab/nav/top_nav_menu_item_spec.rb3
-rw-r--r--spec/lib/gitlab/net_http_adapter_spec.rb3
-rw-r--r--spec/lib/gitlab/observability_spec.rb186
-rw-r--r--spec/lib/gitlab/optimistic_locking_spec.rb13
-rw-r--r--spec/lib/gitlab/pages/random_domain_spec.rb44
-rw-r--r--spec/lib/gitlab/pages/virtual_host_finder_spec.rb214
-rw-r--r--spec/lib/gitlab/patch/node_loader_spec.rb80
-rw-r--r--spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb31
-rw-r--r--spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb89
-rw-r--r--spec/lib/gitlab/rack_attack/store_spec.rb113
-rw-r--r--spec/lib/gitlab/redis/cache_spec.rb17
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb219
-rw-r--r--spec/lib/gitlab/redis/rate_limiting_spec.rb15
-rw-r--r--spec/lib/gitlab/redis/repository_cache_spec.rb15
-rw-r--r--spec/lib/gitlab/regex_spec.rb31
-rw-r--r--spec/lib/gitlab/safe_device_detector_spec.rb2
-rw-r--r--spec/lib/gitlab/sanitizers/exception_message_spec.rb3
-rw-r--r--spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb24
-rw-r--r--spec/lib/gitlab/serverless/service_spec.rb136
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb6
-rw-r--r--spec/lib/gitlab/sidekiq_queue_spec.rb10
-rw-r--r--spec/lib/gitlab/slug/path_spec.rb2
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb99
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb22
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_bulk_imports_entities_metric_spec.rb119
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb26
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb6
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/gitlab_dedicated_metric_spec.rb9
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb30
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric_spec.rb20
-rw-r--r--spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb16
-rw-r--r--spec/lib/gitlab/usage_data_counters/container_registry_event_counter_spec.rb11
-rw-r--r--spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb21
-rw-r--r--spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb13
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb142
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb72
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb70
-rw-r--r--spec/lib/gitlab/utils/error_message_spec.rb23
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb2
-rw-r--r--spec/lib/gitlab/utils/uniquify_spec.rb (renamed from spec/models/concerns/uniquify_spec.rb)10
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb4
-rw-r--r--spec/lib/gitlab/utils/username_and_email_generator_spec.rb24
-rw-r--r--spec/lib/object_storage/config_spec.rb9
-rw-r--r--spec/lib/security/weak_passwords_spec.rb2
-rw-r--r--spec/lib/sidebars/concerns/super_sidebar_panel_spec.rb139
-rw-r--r--spec/lib/sidebars/groups/menus/group_information_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb55
-rw-r--r--spec/lib/sidebars/groups/menus/issues_menu_spec.rb15
-rw-r--r--spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb14
-rw-r--r--spec/lib/sidebars/groups/menus/observability_menu_spec.rb60
-rw-r--r--spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb4
-rw-r--r--spec/lib/sidebars/groups/menus/scope_menu_spec.rb16
-rw-r--r--spec/lib/sidebars/groups/super_sidebar_panel_spec.rb44
-rw-r--r--spec/lib/sidebars/menu_spec.rb79
-rw-r--r--spec/lib/sidebars/panel_spec.rb28
-rw-r--r--spec/lib/sidebars/projects/menus/deployments_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb72
-rw-r--r--spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb52
-rw-r--r--spec/lib/sidebars/projects/menus/issues_menu_spec.rb15
-rw-r--r--spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb15
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/project_information_menu_spec.rb6
-rw-r--r--spec/lib/sidebars/projects/menus/repository_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/scope_menu_spec.rb14
-rw-r--r--spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb4
-rw-r--r--spec/lib/sidebars/projects/menus/snippets_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/projects/menus/wiki_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/projects/panel_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_panel_spec.rb45
-rw-r--r--spec/lib/sidebars/static_menu_spec.rb38
-rw-r--r--spec/lib/sidebars/uncategorized_menu_spec.rb12
-rw-r--r--spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_profile/menus/following_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb11
-rw-r--r--spec/lib/sidebars/user_profile/panel_spec.rb24
-rw-r--r--spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb65
-rw-r--r--spec/lib/sidebars/user_settings/menus/account_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/password_menu_spec.rb38
-rw-r--r--spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb65
-rw-r--r--spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb13
-rw-r--r--spec/lib/sidebars/user_settings/panel_spec.rb15
-rw-r--r--spec/lib/sidebars/your_work/panel_spec.rb15
-rw-r--r--spec/lib/unnested_in_filters/rewriter_spec.rb26
-rw-r--r--spec/mailers/emails/in_product_marketing_spec.rb6
-rw-r--r--spec/mailers/emails/issues_spec.rb36
-rw-r--r--spec/mailers/emails/profile_spec.rb9
-rw-r--r--spec/mailers/emails/work_items_spec.rb17
-rw-r--r--spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb2
-rw-r--r--spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb2
-rw-r--r--spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb2
-rw-r--r--spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb2
-rw-r--r--spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb2
-rw-r--r--spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb2
-rw-r--r--spec/migrations/20221209235940_cleanup_o_auth_access_tokens_with_null_expires_in_spec.rb2
-rw-r--r--spec/migrations/20230118144623_schedule_migration_for_remediation_spec.rb25
-rw-r--r--spec/migrations/20230125195503_queue_backfill_compliance_violations_spec.rb32
-rw-r--r--spec/migrations/20230130182412_schedule_create_vulnerability_links_migration_spec.rb30
-rw-r--r--spec/migrations/20230202211434_migrate_redis_slot_keys_spec.rb54
-rw-r--r--spec/migrations/20230208125736_schedule_migration_for_links_spec.rb31
-rw-r--r--spec/migrations/20230214181633_finalize_ci_build_needs_big_int_conversion_spec.rb43
-rw-r--r--spec/migrations/20230220102212_swap_columns_ci_build_needs_big_int_conversion_spec.rb60
-rw-r--r--spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb22
-rw-r--r--spec/migrations/20230221214519_remove_incorrectly_onboarded_namespaces_from_onboarding_progress_spec.rb59
-rw-r--r--spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb93
-rw-r--r--spec/migrations/20230224085743_update_issues_internal_id_scope_spec.rb28
-rw-r--r--spec/migrations/20230224144233_migrate_evidences_from_raw_metadata_spec.rb31
-rw-r--r--spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb27
-rw-r--r--spec/migrations/20230302185739_queue_fix_vulnerability_reads_has_issues_spec.rb27
-rw-r--r--spec/migrations/20230303105806_queue_delete_orphaned_packages_dependencies_spec.rb26
-rw-r--r--spec/migrations/20230306195007_queue_backfill_project_wiki_repositories_spec.rb26
-rw-r--r--spec/migrations/20230309071242_delete_security_policy_bot_users_spec.rb24
-rw-r--r--spec/migrations/20230313150531_reschedule_migration_for_remediation_spec.rb31
-rw-r--r--spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb2
-rw-r--r--spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb2
-rw-r--r--spec/migrations/ensure_merge_request_metrics_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb37
-rw-r--r--spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb37
-rw-r--r--spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb2
-rw-r--r--spec/migrations/queue_backfill_prepared_at_data_spec.rb24
-rw-r--r--spec/migrations/swap_merge_request_metrics_id_to_bigint_for_gitlab_dot_com_spec.rb76
-rw-r--r--spec/migrations/swap_timelogs_note_id_to_bigint_for_gitlab_dot_com_spec.rb66
-rw-r--r--spec/migrations/update_application_settings_protected_paths_spec.rb2
-rw-r--r--spec/models/abuse_report_spec.rb36
-rw-r--r--spec/models/achievements/user_achievement_spec.rb29
-rw-r--r--spec/models/airflow/dags_spec.rb17
-rw-r--r--spec/models/alert_management/alert_assignee_spec.rb6
-rw-r--r--spec/models/alert_management/alert_spec.rb10
-rw-r--r--spec/models/alert_management/alert_user_mention_spec.rb6
-rw-r--r--spec/models/application_setting_spec.rb113
-rw-r--r--spec/models/audit_event_spec.rb4
-rw-r--r--spec/models/board_spec.rb8
-rw-r--r--spec/models/bulk_import_spec.rb29
-rw-r--r--spec/models/bulk_imports/batch_tracker_spec.rb16
-rw-r--r--spec/models/bulk_imports/entity_spec.rb66
-rw-r--r--spec/models/bulk_imports/export_batch_spec.rb17
-rw-r--r--spec/models/bulk_imports/export_spec.rb3
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb12
-rw-r--r--spec/models/bulk_imports/tracker_spec.rb5
-rw-r--r--spec/models/chat_name_spec.rb7
-rw-r--r--spec/models/ci/build_metadata_spec.rb1
-rw-r--r--spec/models/ci/build_pending_state_spec.rb7
-rw-r--r--spec/models/ci/build_spec.rb160
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb6
-rw-r--r--spec/models/ci/catalog/listing_spec.rb65
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb6
-rw-r--r--spec/models/ci/group_variable_spec.rb2
-rw-r--r--spec/models/ci/job_artifact_spec.rb25
-rw-r--r--spec/models/ci/job_token/scope_spec.rb8
-rw-r--r--spec/models/ci/job_variable_spec.rb2
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb17
-rw-r--r--spec/models/ci/pipeline_spec.rb217
-rw-r--r--spec/models/ci/processable_spec.rb2
-rw-r--r--spec/models/ci/runner_machine_build_spec.rb100
-rw-r--r--spec/models/ci/runner_machine_spec.rb200
-rw-r--r--spec/models/ci/runner_spec.rb235
-rw-r--r--spec/models/ci/runner_version_spec.rb11
-rw-r--r--spec/models/ci/sources/pipeline_spec.rb5
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/clusters/applications/crossplane_spec.rb62
-rw-r--r--spec/models/clusters/applications/knative_spec.rb25
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb349
-rw-r--r--spec/models/clusters/cluster_spec.rb20
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb9
-rw-r--r--spec/models/commit_collection_spec.rb15
-rw-r--r--spec/models/commit_status_spec.rb39
-rw-r--r--spec/models/concerns/atomic_internal_id_spec.rb99
-rw-r--r--spec/models/concerns/ci/maskable_spec.rb2
-rw-r--r--spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb80
-rw-r--r--spec/models/concerns/ci/partitionable_spec.rb10
-rw-r--r--spec/models/concerns/each_batch_spec.rb32
-rw-r--r--spec/models/concerns/has_user_type_spec.rb4
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb52
-rw-r--r--spec/models/concerns/routable_spec.rb11
-rw-r--r--spec/models/concerns/subscribable_spec.rb26
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/base_spec.rb37
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb149
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb92
-rw-r--r--spec/models/concerns/web_hooks/has_web_hooks_spec.rb41
-rw-r--r--spec/models/container_registry/data_repair_detail_spec.rb11
-rw-r--r--spec/models/container_registry/event_spec.rb22
-rw-r--r--spec/models/container_repository_spec.rb29
-rw-r--r--spec/models/design_management/design_spec.rb5
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb38
-rw-r--r--spec/models/group_spec.rb14
-rw-r--r--spec/models/hooks/web_hook_spec.rb22
-rw-r--r--spec/models/import_export_upload_spec.rb2
-rw-r--r--spec/models/import_failure_spec.rb16
-rw-r--r--spec/models/integrations/apple_app_store_spec.rb5
-rw-r--r--spec/models/integrations/campfire_spec.rb14
-rw-r--r--spec/models/integrations/google_play_spec.rb81
-rw-r--r--spec/models/integrations/jira_spec.rb6
-rw-r--r--spec/models/integrations/mattermost_slash_commands_spec.rb9
-rw-r--r--spec/models/integrations/slack_slash_commands_spec.rb9
-rw-r--r--spec/models/integrations/squash_tm_spec.rb117
-rw-r--r--spec/models/internal_id_spec.rb12
-rw-r--r--spec/models/issue_spec.rb136
-rw-r--r--spec/models/member_spec.rb109
-rw-r--r--spec/models/members/member_role_spec.rb107
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb41
-rw-r--r--spec/models/namespace_spec.rb412
-rw-r--r--spec/models/namespaces/randomized_suffix_path_spec.rb2
-rw-r--r--spec/models/note_spec.rb10
-rw-r--r--spec/models/oauth_access_token_spec.rb4
-rw-r--r--spec/models/onboarding/completion_spec.rb73
-rw-r--r--spec/models/packages/debian/file_metadatum_spec.rb1
-rw-r--r--spec/models/packages/package_file_spec.rb1
-rw-r--r--spec/models/pages/lookup_path_spec.rb69
-rw-r--r--spec/models/pages_domain_spec.rb39
-rw-r--r--spec/models/personal_access_token_spec.rb24
-rw-r--r--spec/models/preloaders/runner_machine_policy_preloader_spec.rb38
-rw-r--r--spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb16
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb18
-rw-r--r--spec/models/project_feature_spec.rb34
-rw-r--r--spec/models/project_setting_spec.rb38
-rw-r--r--spec/models/project_spec.rb353
-rw-r--r--spec/models/projects/data_transfer_spec.rb6
-rw-r--r--spec/models/projects/forks/details_spec.rb (renamed from spec/models/projects/forks/divergence_counts_spec.rb)78
-rw-r--r--spec/models/projects/import_export/relation_export_spec.rb22
-rw-r--r--spec/models/protected_branch_spec.rb85
-rw-r--r--spec/models/repository_spec.rb119
-rw-r--r--spec/models/serverless/domain_cluster_spec.rb75
-rw-r--r--spec/models/serverless/domain_spec.rb97
-rw-r--r--spec/models/serverless/function_spec.rb21
-rw-r--r--spec/models/service_desk/custom_email_verification_spec.rb109
-rw-r--r--spec/models/service_desk_setting_spec.rb57
-rw-r--r--spec/models/user_spec.rb229
-rw-r--r--spec/models/wiki_directory_spec.rb9
-rw-r--r--spec/models/wiki_page_spec.rb14
-rw-r--r--spec/models/work_item_spec.rb24
-rw-r--r--spec/models/work_items/widget_definition_spec.rb3
-rw-r--r--spec/models/work_items/widgets/notifications_spec.rb19
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb2
-rw-r--r--spec/policies/ci/runner_machine_policy_spec.rb176
-rw-r--r--spec/policies/design_management/design_policy_spec.rb4
-rw-r--r--spec/policies/global_policy_spec.rb98
-rw-r--r--spec/policies/group_policy_spec.rb37
-rw-r--r--spec/policies/metrics/dashboard/annotation_policy_spec.rb16
-rw-r--r--spec/policies/project_group_link_policy_spec.rb2
-rw-r--r--spec/policies/project_hook_policy_spec.rb6
-rw-r--r--spec/policies/project_policy_spec.rb119
-rw-r--r--spec/presenters/blob_presenter_spec.rb10
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb78
-rw-r--r--spec/presenters/commit_presenter_spec.rb13
-rw-r--r--spec/presenters/issue_presenter_spec.rb20
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb9
-rw-r--r--spec/presenters/packages/detail/package_presenter_spec.rb3
-rw-r--r--spec/rails_autoload.rb56
-rw-r--r--spec/requests/admin/abuse_reports_controller_spec.rb45
-rw-r--r--spec/requests/admin/applications_controller_spec.rb2
-rw-r--r--spec/requests/admin/broadcast_messages_controller_spec.rb5
-rw-r--r--spec/requests/admin/impersonation_tokens_controller_spec.rb2
-rw-r--r--spec/requests/admin/projects_controller_spec.rb58
-rw-r--r--spec/requests/admin/version_check_controller_spec.rb2
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/admin/batched_background_migrations_spec.rb93
-rw-r--r--spec/requests/api/admin/ci/variables_spec.rb121
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb137
-rw-r--r--spec/requests/api/admin/plan_limits_spec.rb58
-rw-r--r--spec/requests/api/admin/sidekiq_spec.rb25
-rw-r--r--spec/requests/api/api_guard/admin_mode_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_guard/response_coercer_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb2
-rw-r--r--spec/requests/api/appearance_spec.rb4
-rw-r--r--spec/requests/api/applications_spec.rb8
-rw-r--r--spec/requests/api/avatar_spec.rb1
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb6
-rw-r--r--spec/requests/api/ci/pipeline_schedules_spec.rb4
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb114
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb11
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb13
-rw-r--r--spec/requests/api/ci/runner/runners_verify_post_spec.rb30
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb15
-rw-r--r--spec/requests/api/ci/runners_spec.rb203
-rw-r--r--spec/requests/api/ci/variables_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb62
-rw-r--r--spec/requests/api/composer_packages_spec.rb6
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb40
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb42
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb2
-rw-r--r--spec/requests/api/draft_notes_spec.rb171
-rw-r--r--spec/requests/api/error_tracking/project_settings_spec.rb244
-rw-r--r--spec/requests/api/files_spec.rb35
-rw-r--r--spec/requests/api/freeze_periods_spec.rb196
-rw-r--r--spec/requests/api/graphql/achievements/user_achievements_query_spec.rb83
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb62
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb80
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb31
-rw-r--r--spec/requests/api/graphql/current_user/todos_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/multiplexed_queries_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/achievements/award_spec.rb106
-rw-r--r--spec/requests/api/graphql/mutations/achievements/revoke_spec.rb91
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb)4
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/play_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_play_spec.rb)4
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/retry_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_retry_spec.rb)8
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb)2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb197
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/ci/runner/create_spec.rb121
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/update_spec.rb77
-rw-r--r--spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb68
-rw-r--r--spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb128
-rw-r--r--spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb131
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb54
-rw-r--r--spec/requests/api/graphql/mutations/work_items/export_spec.rb67
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb137
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/flow_metrics_spec.rb23
-rw-r--r--spec/requests/api/graphql/project/fork_details_spec.rb35
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb28
-rw-r--r--spec/requests/api/graphql/query_spec.rb2
-rw-r--r--spec/requests/api/graphql/user/user_achievements_query_spec.rb91
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb26
-rw-r--r--spec/requests/api/graphql_spec.rb2
-rw-r--r--spec/requests/api/group_milestones_spec.rb78
-rw-r--r--spec/requests/api/group_variables_spec.rb2
-rw-r--r--spec/requests/api/helm_packages_spec.rb15
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal/base_spec.rb12
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb147
-rw-r--r--spec/requests/api/internal/pages_spec.rb378
-rw-r--r--spec/requests/api/internal/workhorse_spec.rb2
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb30
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb42
-rw-r--r--spec/requests/api/issues/issues_spec.rb67
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb90
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb62
-rw-r--r--spec/requests/api/keys_spec.rb2
-rw-r--r--spec/requests/api/lint_spec.rb10
-rw-r--r--spec/requests/api/maven_packages_spec.rb37
-rw-r--r--spec/requests/api/merge_requests_spec.rb11
-rw-r--r--spec/requests/api/metadata_spec.rb2
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb34
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb4
-rw-r--r--spec/requests/api/nuget_group_packages_spec.rb12
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb11
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb4
-rw-r--r--spec/requests/api/pages/internal_access_spec.rb68
-rw-r--r--spec/requests/api/pages/pages_spec.rb12
-rw-r--r--spec/requests/api/pages/private_access_spec.rb68
-rw-r--r--spec/requests/api/pages/public_access_spec.rb68
-rw-r--r--spec/requests/api/pages_domains_spec.rb40
-rw-r--r--spec/requests/api/personal_access_tokens/self_information_spec.rb2
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb2
-rw-r--r--spec/requests/api/project_milestones_spec.rb87
-rw-r--r--spec/requests/api/projects_spec.rb105
-rw-r--r--spec/requests/api/protected_branches_spec.rb9
-rw-r--r--spec/requests/api/pypi_packages_spec.rb30
-rw-r--r--spec/requests/api/release/links_spec.rb24
-rw-r--r--spec/requests/api/repositories_spec.rb1
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb2
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb30
-rw-r--r--spec/requests/api/search_spec.rb17
-rw-r--r--spec/requests/api/settings_spec.rb10
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb2
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb7
-rw-r--r--spec/requests/api/terraform/state_spec.rb9
-rw-r--r--spec/requests/api/terraform/state_version_spec.rb8
-rw-r--r--spec/requests/api/unleash_spec.rb8
-rw-r--r--spec/requests/api/users_spec.rb12
-rw-r--r--spec/requests/dashboard_controller_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb24
-rw-r--r--spec/requests/groups/email_campaigns_controller_spec.rb6
-rw-r--r--spec/requests/groups/observability_controller_spec.rb18
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/groups/settings/applications_controller_spec.rb2
-rw-r--r--spec/requests/ide_controller_spec.rb120
-rw-r--r--spec/requests/jira_connect/oauth_application_ids_controller_spec.rb6
-rw-r--r--spec/requests/jira_connect/public_keys_controller_spec.rb21
-rw-r--r--spec/requests/jwks_controller_spec.rb2
-rw-r--r--spec/requests/jwt_controller_spec.rb10
-rw-r--r--spec/requests/oauth/applications_controller_spec.rb2
-rw-r--r--spec/requests/oauth/authorizations_controller_spec.rb2
-rw-r--r--spec/requests/oauth/tokens_controller_spec.rb2
-rw-r--r--spec/requests/oauth_tokens_spec.rb2
-rw-r--r--spec/requests/openid_connect_spec.rb2
-rw-r--r--spec/requests/projects/airflow/dags_controller_spec.rb105
-rw-r--r--spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb112
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb16
-rw-r--r--spec/requests/projects/issues_controller_spec.rb29
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb261
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/projects/uploads_spec.rb2
-rw-r--r--spec/requests/projects/wikis_controller_spec.rb73
-rw-r--r--spec/requests/rack_attack_global_spec.rb2
-rw-r--r--spec/requests/sandbox_controller_spec.rb2
-rw-r--r--spec/requests/sessions_spec.rb2
-rw-r--r--spec/requests/users_controller_spec.rb100
-rw-r--r--spec/requests/verifies_with_email_spec.rb2
-rw-r--r--spec/routing/import_routing_spec.rb4
-rw-r--r--spec/routing/user_routing_spec.rb2
-rw-r--r--spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb137
-rw-r--r--spec/rubocop/cop/gitlab/doc_url_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb10
-rw-r--r--spec/rubocop/cop/lint/last_keyword_argument_spec.rb2
-rw-r--r--spec/rubocop/cop/rspec/avoid_test_prof_spec.rb2
-rw-r--r--spec/scripts/database/schema_validator_spec.rb67
-rw-r--r--spec/scripts/generate_rspec_pipeline_spec.rb198
-rw-r--r--spec/scripts/lib/glfm/shared_spec.rb3
-rw-r--r--spec/scripts/pipeline/create_test_failure_issues_spec.rb145
-rw-r--r--spec/scripts/pipeline_test_report_builder_spec.rb5
-rw-r--r--spec/scripts/review_apps/automated_cleanup_spec.rb261
-rw-r--r--spec/serializers/admin/abuse_report_entity_spec.rb32
-rw-r--r--spec/serializers/admin/abuse_report_serializer_spec.rb23
-rw-r--r--spec/serializers/build_details_entity_spec.rb2
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb81
-rw-r--r--spec/serializers/cluster_entity_spec.rb14
-rw-r--r--spec/serializers/cluster_serializer_spec.rb4
-rw-r--r--spec/serializers/entity_date_helper_spec.rb10
-rw-r--r--spec/serializers/group_issuable_autocomplete_entity_spec.rb3
-rw-r--r--spec/serializers/issue_board_entity_spec.rb10
-rw-r--r--spec/serializers/issue_entity_spec.rb12
-rw-r--r--spec/serializers/linked_project_issue_entity_spec.rb10
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb28
-rw-r--r--spec/serializers/profile/event_entity_spec.rb149
-rw-r--r--spec/serializers/project_import_entity_spec.rb30
-rw-r--r--spec/services/access_token_validation_service_spec.rb2
-rw-r--r--spec/services/achievements/award_service_spec.rb73
-rw-r--r--spec/services/achievements/revoke_service_spec.rb66
-rw-r--r--spec/services/admin/set_feature_flag_service_spec.rb2
-rw-r--r--spec/services/alert_management/alerts/todo/create_service_spec.rb2
-rw-r--r--spec/services/alert_management/alerts/update_service_spec.rb2
-rw-r--r--spec/services/alert_management/create_alert_issue_service_spec.rb2
-rw-r--r--spec/services/alert_management/http_integrations/create_service_spec.rb2
-rw-r--r--spec/services/alert_management/http_integrations/destroy_service_spec.rb2
-rw-r--r--spec/services/alert_management/http_integrations/update_service_spec.rb2
-rw-r--r--spec/services/alert_management/metric_images/upload_service_spec.rb2
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb2
-rw-r--r--spec/services/analytics/cycle_analytics/stages/list_service_spec.rb2
-rw-r--r--spec/services/application_settings/update_service_spec.rb6
-rw-r--r--spec/services/audit_event_service_spec.rb2
-rw-r--r--spec/services/audit_events/build_service_spec.rb2
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb2
-rw-r--r--spec/services/auth/dependency_proxy_authentication_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/find_records_due_for_refresh_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/periodic_recalculate_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/project_access_changed_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/project_recalculate_per_user_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/project_recalculate_service_spec.rb2
-rw-r--r--spec/services/auto_merge/base_service_spec.rb2
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb2
-rw-r--r--spec/services/auto_merge_service_spec.rb2
-rw-r--r--spec/services/award_emojis/add_service_spec.rb2
-rw-r--r--spec/services/award_emojis/base_service_spec.rb2
-rw-r--r--spec/services/award_emojis/collect_user_emoji_service_spec.rb2
-rw-r--r--spec/services/award_emojis/copy_service_spec.rb2
-rw-r--r--spec/services/award_emojis/destroy_service_spec.rb2
-rw-r--r--spec/services/award_emojis/toggle_service_spec.rb2
-rw-r--r--spec/services/base_container_service_spec.rb2
-rw-r--r--spec/services/base_count_service_spec.rb2
-rw-r--r--spec/services/boards/create_service_spec.rb2
-rw-r--r--spec/services/boards/destroy_service_spec.rb2
-rw-r--r--spec/services/boards/issues/create_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb2
-rw-r--r--spec/services/boards/issues/move_service_spec.rb2
-rw-r--r--spec/services/boards/lists/create_service_spec.rb2
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb2
-rw-r--r--spec/services/boards/lists/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/move_service_spec.rb2
-rw-r--r--spec/services/boards/lists/update_service_spec.rb2
-rw-r--r--spec/services/boards/visits/create_service_spec.rb2
-rw-r--r--spec/services/branches/create_service_spec.rb4
-rw-r--r--spec/services/branches/delete_merged_service_spec.rb2
-rw-r--r--spec/services/branches/delete_service_spec.rb2
-rw-r--r--spec/services/branches/diverging_commit_counts_service_spec.rb2
-rw-r--r--spec/services/branches/validate_new_service_spec.rb2
-rw-r--r--spec/services/bulk_create_integration_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/archive_extraction_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/file_decompression_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/file_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/lfs_objects_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/relation_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/repository_bundle_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/tree_export_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/uploads_export_service_spec.rb2
-rw-r--r--spec/services/bulk_push_event_payload_service_spec.rb2
-rw-r--r--spec/services/bulk_update_integration_service_spec.rb2
-rw-r--r--spec/services/captcha/captcha_verification_service_spec.rb2
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb2
-rw-r--r--spec/services/ci/abort_pipelines_service_spec.rb2
-rw-r--r--spec/services/ci/append_build_trace_service_spec.rb2
-rw-r--r--spec/services/ci/build_cancel_service_spec.rb2
-rw-r--r--spec/services/ci/build_erase_service_spec.rb2
-rw-r--r--spec/services/ci/build_report_result_service_spec.rb2
-rw-r--r--spec/services/ci/build_unschedule_service_spec.rb2
-rw-r--r--spec/services/ci/catalog/add_resource_service_spec.rb55
-rw-r--r--spec/services/ci/catalog/validate_resource_service_spec.rb57
-rw-r--r--spec/services/ci/change_variable_service_spec.rb2
-rw-r--r--spec/services/ci/change_variables_service_spec.rb2
-rw-r--r--spec/services/ci/compare_accessibility_reports_service_spec.rb2
-rw-r--r--spec/services/ci/compare_codequality_reports_service_spec.rb2
-rw-r--r--spec/services/ci/compare_reports_base_service_spec.rb2
-rw-r--r--spec/services/ci/compare_test_reports_service_spec.rb2
-rw-r--r--spec/services/ci/components/fetch_service_spec.rb2
-rw-r--r--spec/services/ci/copy_cross_database_associations_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/artifacts_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/cache_spec.rb3
-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.rb3
-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.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/dry_run_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/environment_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/include_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/limit_active_jobs_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/logger_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/merge_requests_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/needs_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/parallel_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/parameter_content_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/pre_post_stages_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/rate_limit_spec.rb3
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb36
-rw-r--r--spec/services/ci/create_pipeline_service/scripts_spec.rb28
-rw-r--r--spec/services/ci/create_pipeline_service/tags_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/variables_spec.rb3
-rw-r--r--spec/services/ci/create_web_ide_terminal_service_spec.rb2
-rw-r--r--spec/services/ci/daily_build_group_report_result_service_spec.rb2
-rw-r--r--spec/services/ci/delete_objects_service_spec.rb2
-rw-r--r--spec/services/ci/delete_unit_tests_service_spec.rb2
-rw-r--r--spec/services/ci/deployments/destroy_service_spec.rb2
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/destroy_secure_file_service_spec.rb2
-rw-r--r--spec/services/ci/disable_user_pipeline_schedules_service_spec.rb2
-rw-r--r--spec/services/ci/drop_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/ensure_stage_service_spec.rb2
-rw-r--r--spec/services/ci/expire_pipeline_cache_service_spec.rb2
-rw-r--r--spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/find_exposed_artifacts_service_spec.rb2
-rw-r--r--spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb2
-rw-r--r--spec/services/ci/generate_coverage_reports_service_spec.rb2
-rw-r--r--spec/services/ci/generate_kubeconfig_service_spec.rb2
-rw-r--r--spec/services/ci/generate_terraform_reports_service_spec.rb2
-rw-r--r--spec/services/ci/job_artifacts/bulk_delete_by_project_service_spec.rb121
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb145
-rw-r--r--spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb2
-rw-r--r--spec/services/ci/job_artifacts/delete_service_spec.rb2
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb38
-rw-r--r--spec/services/ci/job_artifacts/destroy_associations_service_spec.rb31
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb15
-rw-r--r--spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb2
-rw-r--r--spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb2
-rw-r--r--spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb3
-rw-r--r--spec/services/ci/list_config_variables_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb3
-rw-r--r--spec/services/ci/pipeline_bridge_status_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb25
-rw-r--r--spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb2
-rw-r--r--spec/services/ci/pipelines/add_job_service_spec.rb2
-rw-r--r--spec/services/ci/pipelines/hook_service_spec.rb2
-rw-r--r--spec/services/ci/play_bridge_service_spec.rb2
-rw-r--r--spec/services/ci/play_build_service_spec.rb2
-rw-r--r--spec/services/ci/play_manual_stage_service_spec.rb2
-rw-r--r--spec/services/ci/prepare_build_service_spec.rb2
-rw-r--r--spec/services/ci/process_build_service_spec.rb2
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/process_sync_events_service_spec.rb2
-rw-r--r--spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb2
-rw-r--r--spec/services/ci/queue/pending_builds_strategy_spec.rb2
-rw-r--r--spec/services/ci/register_job_service_spec.rb1160
-rw-r--r--spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb2
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/run_scheduled_build_service_spec.rb2
-rw-r--r--spec/services/ci/runners/create_runner_service_spec.rb43
-rw-r--r--spec/services/ci/runners/process_runner_version_update_service_spec.rb13
-rw-r--r--spec/services/ci/stuck_builds/drop_pending_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_running_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb2
-rw-r--r--spec/services/ci/test_failure_history_service_spec.rb2
-rw-r--r--spec/services/ci/track_failed_build_service_spec.rb2
-rw-r--r--spec/services/ci/unlock_artifacts_service_spec.rb10
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb2
-rw-r--r--spec/services/ci/update_instance_variables_service_spec.rb2
-rw-r--r--spec/services/ci/update_pending_build_service_spec.rb2
-rw-r--r--spec/services/clusters/agent_tokens/create_service_spec.rb8
-rw-r--r--spec/services/clusters/agent_tokens/revoke_service_spec.rb77
-rw-r--r--spec/services/clusters/agent_tokens/track_usage_service_spec.rb2
-rw-r--r--spec/services/clusters/agents/authorize_proxy_user_service_spec.rb68
-rw-r--r--spec/services/clusters/agents/create_activity_event_service_spec.rb13
-rw-r--r--spec/services/clusters/agents/create_service_spec.rb2
-rw-r--r--spec/services/clusters/agents/delete_expired_events_service_spec.rb2
-rw-r--r--spec/services/clusters/agents/delete_service_spec.rb2
-rw-r--r--spec/services/clusters/build_kubernetes_namespace_service_spec.rb2
-rw-r--r--spec/services/clusters/build_service_spec.rb2
-rw-r--r--spec/services/clusters/cleanup/project_namespace_service_spec.rb2
-rw-r--r--spec/services/clusters/cleanup/service_account_service_spec.rb2
-rw-r--r--spec/services/clusters/create_service_spec.rb2
-rw-r--r--spec/services/clusters/destroy_service_spec.rb2
-rw-r--r--spec/services/clusters/integrations/create_service_spec.rb2
-rw-r--r--spec/services/clusters/integrations/prometheus_health_check_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes_spec.rb2
-rw-r--r--spec/services/clusters/management/validate_management_project_permissions_service_spec.rb2
-rw-r--r--spec/services/clusters/update_service_spec.rb2
-rw-r--r--spec/services/cohorts_service_spec.rb2
-rw-r--r--spec/services/commits/cherry_pick_service_spec.rb2
-rw-r--r--spec/services/commits/commit_patch_service_spec.rb2
-rw-r--r--spec/services/commits/tag_service_spec.rb2
-rw-r--r--spec/services/compare_service_spec.rb2
-rw-r--r--spec/services/concerns/audit_event_save_type_spec.rb2
-rw-r--r--spec/services/concerns/exclusive_lease_guard_spec.rb2
-rw-r--r--spec/services/concerns/merge_requests/assigns_merge_params_spec.rb2
-rw-r--r--spec/services/concerns/rate_limited_service_spec.rb2
-rw-r--r--spec/services/container_expiration_policies/cleanup_service_spec.rb3
-rw-r--r--spec/services/container_expiration_policies/update_service_spec.rb2
-rw-r--r--spec/services/customer_relations/contacts/create_service_spec.rb2
-rw-r--r--spec/services/customer_relations/contacts/update_service_spec.rb2
-rw-r--r--spec/services/customer_relations/organizations/create_service_spec.rb2
-rw-r--r--spec/services/customer_relations/organizations/update_service_spec.rb2
-rw-r--r--spec/services/database/consistency_fix_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/auth_token_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/find_cached_manifest_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/group_settings/update_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/head_manifest_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/request_token_service_spec.rb2
-rw-r--r--spec/services/deploy_keys/create_service_spec.rb2
-rw-r--r--spec/services/deployments/archive_in_project_service_spec.rb2
-rw-r--r--spec/services/deployments/create_for_build_service_spec.rb2
-rw-r--r--spec/services/deployments/create_service_spec.rb2
-rw-r--r--spec/services/deployments/link_merge_requests_service_spec.rb2
-rw-r--r--spec/services/deployments/older_deployments_drop_service_spec.rb2
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb2
-rw-r--r--spec/services/deployments/update_service_spec.rb2
-rw-r--r--spec/services/design_management/copy_design_collection/copy_service_spec.rb3
-rw-r--r--spec/services/design_management/copy_design_collection/queue_service_spec.rb3
-rw-r--r--spec/services/design_management/delete_designs_service_spec.rb2
-rw-r--r--spec/services/design_management/design_user_notes_count_service_spec.rb2
-rw-r--r--spec/services/design_management/generate_image_versions_service_spec.rb2
-rw-r--r--spec/services/design_management/move_designs_service_spec.rb2
-rw-r--r--spec/services/discussions/capture_diff_note_position_service_spec.rb2
-rw-r--r--spec/services/discussions/capture_diff_note_positions_service_spec.rb2
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb2
-rw-r--r--spec/services/draft_notes/create_service_spec.rb2
-rw-r--r--spec/services/draft_notes/destroy_service_spec.rb2
-rw-r--r--spec/services/draft_notes/publish_service_spec.rb2
-rw-r--r--spec/services/emails/confirm_service_spec.rb2
-rw-r--r--spec/services/emails/create_service_spec.rb2
-rw-r--r--spec/services/emails/destroy_service_spec.rb2
-rw-r--r--spec/services/environments/auto_stop_service_spec.rb3
-rw-r--r--spec/services/environments/canary_ingress/update_service_spec.rb3
-rw-r--r--spec/services/environments/create_for_build_service_spec.rb2
-rw-r--r--spec/services/environments/reset_auto_stop_service_spec.rb2
-rw-r--r--spec/services/environments/schedule_to_delete_review_apps_service_spec.rb2
-rw-r--r--spec/services/environments/stop_service_spec.rb2
-rw-r--r--spec/services/error_tracking/base_service_spec.rb2
-rw-r--r--spec/services/error_tracking/collect_error_service_spec.rb2
-rw-r--r--spec/services/error_tracking/issue_details_service_spec.rb2
-rw-r--r--spec/services/error_tracking/issue_latest_event_service_spec.rb2
-rw-r--r--spec/services/error_tracking/issue_update_service_spec.rb2
-rw-r--r--spec/services/error_tracking/list_issues_service_spec.rb2
-rw-r--r--spec/services/event_create_service_spec.rb59
-rw-r--r--spec/services/events/destroy_service_spec.rb2
-rw-r--r--spec/services/events/render_service_spec.rb2
-rw-r--r--spec/services/feature_flags/create_service_spec.rb10
-rw-r--r--spec/services/feature_flags/destroy_service_spec.rb5
-rw-r--r--spec/services/feature_flags/hook_service_spec.rb2
-rw-r--r--spec/services/feature_flags/update_service_spec.rb7
-rw-r--r--spec/services/files/create_service_spec.rb2
-rw-r--r--spec/services/files/delete_service_spec.rb6
-rw-r--r--spec/services/files/multi_service_spec.rb16
-rw-r--r--spec/services/files/update_service_spec.rb6
-rw-r--r--spec/services/git/base_hooks_service_spec.rb2
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb2
-rw-r--r--spec/services/git/branch_push_service_spec.rb2
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb2
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb2
-rw-r--r--spec/services/git/tag_push_service_spec.rb2
-rw-r--r--spec/services/git/wiki_push_service/change_spec.rb2
-rw-r--r--spec/services/google_cloud/create_cloudsql_instance_service_spec.rb2
-rw-r--r--spec/services/google_cloud/create_service_accounts_service_spec.rb2
-rw-r--r--spec/services/google_cloud/enable_cloud_run_service_spec.rb2
-rw-r--r--spec/services/google_cloud/enable_cloudsql_service_spec.rb2
-rw-r--r--spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb2
-rw-r--r--spec/services/google_cloud/generate_pipeline_service_spec.rb2
-rw-r--r--spec/services/google_cloud/get_cloudsql_instances_service_spec.rb2
-rw-r--r--spec/services/google_cloud/service_accounts_service_spec.rb2
-rw-r--r--spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb2
-rw-r--r--spec/services/gpg_keys/create_service_spec.rb2
-rw-r--r--spec/services/grafana/proxy_service_spec.rb2
-rw-r--r--spec/services/gravatar_service_spec.rb2
-rw-r--r--spec/services/groups/auto_devops_service_spec.rb2
-rw-r--r--spec/services/groups/autocomplete_service_spec.rb2
-rw-r--r--spec/services/groups/deploy_tokens/create_service_spec.rb2
-rw-r--r--spec/services/groups/deploy_tokens/destroy_service_spec.rb2
-rw-r--r--spec/services/groups/deploy_tokens/revoke_service_spec.rb2
-rw-r--r--spec/services/groups/group_links/create_service_spec.rb2
-rw-r--r--spec/services/groups/group_links/destroy_service_spec.rb2
-rw-r--r--spec/services/groups/group_links/update_service_spec.rb4
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb2
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb2
-rw-r--r--spec/services/groups/merge_requests_count_service_spec.rb2
-rw-r--r--spec/services/groups/nested_create_service_spec.rb2
-rw-r--r--spec/services/groups/open_issues_count_service_spec.rb2
-rw-r--r--spec/services/groups/participants_service_spec.rb2
-rw-r--r--spec/services/groups/update_service_spec.rb2
-rw-r--r--spec/services/groups/update_shared_runners_service_spec.rb2
-rw-r--r--spec/services/groups/update_statistics_service_spec.rb2
-rw-r--r--spec/services/ide/base_config_service_spec.rb2
-rw-r--r--spec/services/ide/schemas_config_service_spec.rb2
-rw-r--r--spec/services/ide/terminal_config_service_spec.rb2
-rw-r--r--spec/services/import/bitbucket_server_service_spec.rb2
-rw-r--r--spec/services/import/fogbugz_service_spec.rb2
-rw-r--r--spec/services/import/github/cancel_project_import_service_spec.rb14
-rw-r--r--spec/services/import/github/notes/create_service_spec.rb2
-rw-r--r--spec/services/import/github_service_spec.rb2
-rw-r--r--spec/services/import/gitlab_projects/create_project_service_spec.rb11
-rw-r--r--spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb2
-rw-r--r--spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb2
-rw-r--r--spec/services/import/prepare_service_spec.rb2
-rw-r--r--spec/services/import/validate_remote_git_endpoint_service_spec.rb24
-rw-r--r--spec/services/import_csv/base_service_spec.rb69
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb2
-rw-r--r--spec/services/incident_management/incidents/create_service_spec.rb2
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb3
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb2
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb2
-rw-r--r--spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb3
-rw-r--r--spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb2
-rw-r--r--spec/services/incident_management/pager_duty/process_webhook_service_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_event_tags/create_service_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb4
-rw-r--r--spec/services/incident_management/timeline_events/destroy_service_spec.rb3
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb1
-rw-r--r--spec/services/integrations/propagate_service_spec.rb2
-rw-r--r--spec/services/integrations/test/project_service_spec.rb2
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb2
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb2
-rw-r--r--spec/services/issuable/destroy_label_links_service_spec.rb2
-rw-r--r--spec/services/issuable/destroy_service_spec.rb2
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb2
-rw-r--r--spec/services/issuable/process_assignees_spec.rb2
-rw-r--r--spec/services/issue_links/create_service_spec.rb3
-rw-r--r--spec/services/issue_links/destroy_service_spec.rb3
-rw-r--r--spec/services/issue_links/list_service_spec.rb2
-rw-r--r--spec/services/issues/after_create_service_spec.rb2
-rw-r--r--spec/services/issues/base_service_spec.rb9
-rw-r--r--spec/services/issues/build_service_spec.rb2
-rw-r--r--spec/services/issues/clone_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb3
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/duplicate_service_spec.rb2
-rw-r--r--spec/services/issues/import_csv_service_spec.rb3
-rw-r--r--spec/services/issues/issuable_base_service_spec.rb9
-rw-r--r--spec/services/issues/prepare_import_csv_service_spec.rb2
-rw-r--r--spec/services/issues/referenced_merge_requests_service_spec.rb2
-rw-r--r--spec/services/issues/related_branches_service_spec.rb2
-rw-r--r--spec/services/issues/relative_position_rebalancing_service_spec.rb2
-rw-r--r--spec/services/issues/reopen_service_spec.rb3
-rw-r--r--spec/services/issues/reorder_service_spec.rb2
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb14
-rw-r--r--spec/services/issues/set_crm_contacts_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb4
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb3
-rw-r--r--spec/services/jira/requests/projects/list_service_spec.rb2
-rw-r--r--spec/services/jira_connect/sync_service_spec.rb2
-rw-r--r--spec/services/jira_connect_installations/destroy_service_spec.rb2
-rw-r--r--spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb6
-rw-r--r--spec/services/jira_connect_subscriptions/create_service_spec.rb2
-rw-r--r--spec/services/jira_import/cloud_users_mapper_service_spec.rb2
-rw-r--r--spec/services/jira_import/server_users_mapper_service_spec.rb2
-rw-r--r--spec/services/jira_import/start_import_service_spec.rb2
-rw-r--r--spec/services/jira_import/users_importer_spec.rb2
-rw-r--r--spec/services/keys/create_service_spec.rb2
-rw-r--r--spec/services/keys/destroy_service_spec.rb2
-rw-r--r--spec/services/keys/expiry_notification_service_spec.rb2
-rw-r--r--spec/services/keys/last_used_service_spec.rb2
-rw-r--r--spec/services/keys/revoke_service_spec.rb13
-rw-r--r--spec/services/labels/available_labels_service_spec.rb2
-rw-r--r--spec/services/labels/create_service_spec.rb2
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb2
-rw-r--r--spec/services/labels/promote_service_spec.rb2
-rw-r--r--spec/services/labels/transfer_service_spec.rb2
-rw-r--r--spec/services/labels/update_service_spec.rb2
-rw-r--r--spec/services/lfs/lock_file_service_spec.rb2
-rw-r--r--spec/services/lfs/locks_finder_service_spec.rb2
-rw-r--r--spec/services/lfs/push_service_spec.rb2
-rw-r--r--spec/services/lfs/unlock_file_service_spec.rb2
-rw-r--r--spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb2
-rw-r--r--spec/services/loose_foreign_keys/cleaner_service_spec.rb2
-rw-r--r--spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb2
-rw-r--r--spec/services/markdown_content_rewriter_service_spec.rb2
-rw-r--r--spec/services/markup/rendering_service_spec.rb19
-rw-r--r--spec/services/mattermost/create_team_service_spec.rb28
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb2
-rw-r--r--spec/services/members/create_service_spec.rb3
-rw-r--r--spec/services/members/creator_service_spec.rb2
-rw-r--r--spec/services/members/groups/creator_service_spec.rb2
-rw-r--r--spec/services/members/import_project_team_service_spec.rb2
-rw-r--r--spec/services/members/invitation_reminder_email_service_spec.rb2
-rw-r--r--spec/services/members/invite_member_builder_spec.rb2
-rw-r--r--spec/services/members/invite_service_spec.rb3
-rw-r--r--spec/services/members/projects/creator_service_spec.rb2
-rw-r--r--spec/services/members/request_access_service_spec.rb2
-rw-r--r--spec/services/members/standard_member_builder_spec.rb2
-rw-r--r--spec/services/members/unassign_issuables_service_spec.rb2
-rw-r--r--spec/services/members/update_service_spec.rb2
-rw-r--r--spec/services/merge_requests/add_context_service_spec.rb2
-rw-r--r--spec/services/merge_requests/add_spent_time_service_spec.rb2
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb2
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb2
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb2
-rw-r--r--spec/services/merge_requests/base_service_spec.rb4
-rw-r--r--spec/services/merge_requests/cleanup_refs_service_spec.rb2
-rw-r--r--spec/services/merge_requests/close_service_spec.rb6
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_approval_event_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb7
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb3
-rw-r--r--spec/services/merge_requests/execute_approval_hooks_service_spec.rb2
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb2
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb2
-rw-r--r--spec/services/merge_requests/handle_assignees_change_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_orchestration_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/check_base_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/check_open_status_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/logger_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb2
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb2
-rw-r--r--spec/services/merge_requests/migrate_external_diffs_service_spec.rb2
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb8
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb8
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb3
-rw-r--r--spec/services/merge_requests/reload_merge_head_diff_service_spec.rb2
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb8
-rw-r--r--spec/services/merge_requests/request_review_service_spec.rb2
-rw-r--r--spec/services/merge_requests/resolve_todos_service_spec.rb2
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service_spec.rb2
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_assignees_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_reviewers_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/annotations/create_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/annotations/delete_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/clone_dashboard_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/custom_dashboard_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/default_embed_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/dynamic_embed_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/panel_preview_service_spec.rb2
-rw-r--r--spec/services/metrics/dashboard/pod_dashboard_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/self_monitoring_dashboard_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/system_dashboard_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/transient_embed_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/update_dashboard_service_spec.rb2
-rw-r--r--spec/services/metrics/sample_metrics_service_spec.rb2
-rw-r--r--spec/services/metrics/users_starred_dashboards/create_service_spec.rb2
-rw-r--r--spec/services/metrics/users_starred_dashboards/delete_service_spec.rb2
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/milestones/closed_issues_count_service_spec.rb3
-rw-r--r--spec/services/milestones/create_service_spec.rb2
-rw-r--r--spec/services/milestones/destroy_service_spec.rb2
-rw-r--r--spec/services/milestones/find_or_create_service_spec.rb2
-rw-r--r--spec/services/milestones/issues_count_service_spec.rb3
-rw-r--r--spec/services/milestones/merge_requests_count_service_spec.rb3
-rw-r--r--spec/services/milestones/promote_service_spec.rb2
-rw-r--r--spec/services/milestones/transfer_service_spec.rb2
-rw-r--r--spec/services/milestones/update_service_spec.rb2
-rw-r--r--spec/services/ml/experiment_tracking/candidate_repository_spec.rb2
-rw-r--r--spec/services/ml/experiment_tracking/experiment_repository_spec.rb2
-rw-r--r--spec/services/namespace_settings/update_service_spec.rb2
-rw-r--r--spec/services/namespaces/in_product_marketing_emails_service_spec.rb2
-rw-r--r--spec/services/namespaces/package_settings/update_service_spec.rb2
-rw-r--r--spec/services/namespaces/statistics_refresher_service_spec.rb2
-rw-r--r--spec/services/note_summary_spec.rb2
-rw-r--r--spec/services/notes/build_service_spec.rb2
-rw-r--r--spec/services/notes/copy_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb5
-rw-r--r--spec/services/notes/destroy_service_spec.rb2
-rw-r--r--spec/services/notes/post_process_service_spec.rb2
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb86
-rw-r--r--spec/services/notes/render_service_spec.rb2
-rw-r--r--spec/services/notes/resolve_service_spec.rb2
-rw-r--r--spec/services/notes/update_service_spec.rb2
-rw-r--r--spec/services/notification_recipients/build_service_spec.rb2
-rw-r--r--spec/services/notification_recipients/builder/default_spec.rb2
-rw-r--r--spec/services/notification_recipients/builder/new_note_spec.rb2
-rw-r--r--spec/services/onboarding/progress_service_spec.rb2
-rw-r--r--spec/services/packages/cleanup/execute_policy_service_spec.rb2
-rw-r--r--spec/services/packages/cleanup/update_policy_service_spec.rb2
-rw-r--r--spec/services/packages/composer/composer_json_service_spec.rb2
-rw-r--r--spec/services/packages/composer/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/composer/version_parser_service_spec.rb2
-rw-r--r--spec/services/packages/conan/create_package_file_service_spec.rb2
-rw-r--r--spec/services/packages/conan/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb2
-rw-r--r--spec/services/packages/create_event_service_spec.rb2
-rw-r--r--spec/services/packages/create_package_file_service_spec.rb2
-rw-r--r--spec/services/packages/create_temporary_package_service_spec.rb2
-rw-r--r--spec/services/packages/debian/create_package_file_service_spec.rb2
-rw-r--r--spec/services/packages/debian/extract_changes_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/debian/extract_metadata_service_spec.rb86
-rw-r--r--spec/services/packages/debian/generate_distribution_service_spec.rb28
-rw-r--r--spec/services/packages/debian/parse_debian822_service_spec.rb20
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb2
-rw-r--r--spec/services/packages/debian/process_package_file_service_spec.rb5
-rw-r--r--spec/services/packages/generic/create_package_file_service_spec.rb2
-rw-r--r--spec/services/packages/generic/find_or_create_package_service_spec.rb2
-rw-r--r--spec/services/packages/go/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/go/sync_packages_service_spec.rb2
-rw-r--r--spec/services/packages/helm/extract_file_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/helm/process_file_service_spec.rb2
-rw-r--r--spec/services/packages/mark_package_files_for_destruction_service_spec.rb3
-rw-r--r--spec/services/packages/mark_package_for_destruction_service_spec.rb8
-rw-r--r--spec/services/packages/mark_packages_for_destruction_service_spec.rb7
-rw-r--r--spec/services/packages/maven/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/maven/find_or_create_package_service_spec.rb51
-rw-r--r--spec/services/packages/maven/metadata/append_package_file_service_spec.rb2
-rw-r--r--spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb2
-rw-r--r--spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb2
-rw-r--r--spec/services/packages/maven/metadata/sync_service_spec.rb2
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/npm/create_tag_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/create_dependency_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/search_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/sync_metadatum_service_spec.rb2
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/pypi/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/remove_tag_service_spec.rb2
-rw-r--r--spec/services/packages/rpm/parse_package_service_spec.rb2
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb2
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb2
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb2
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb2
-rw-r--r--spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb2
-rw-r--r--spec/services/packages/rubygems/create_dependencies_service_spec.rb2
-rw-r--r--spec/services/packages/rubygems/create_gemspec_service_spec.rb2
-rw-r--r--spec/services/packages/rubygems/dependency_resolver_service_spec.rb2
-rw-r--r--spec/services/packages/rubygems/metadata_extraction_service_spec.rb2
-rw-r--r--spec/services/packages/rubygems/process_gem_service_spec.rb2
-rw-r--r--spec/services/packages/terraform_module/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/update_package_file_service_spec.rb2
-rw-r--r--spec/services/packages/update_tags_service_spec.rb2
-rw-r--r--spec/services/pages/delete_service_spec.rb2
-rw-r--r--spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb2
-rw-r--r--spec/services/pages/zip_directory_service_spec.rb2
-rw-r--r--spec/services/pages_domains/create_acme_order_service_spec.rb2
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb2
-rw-r--r--spec/services/personal_access_tokens/create_service_spec.rb20
-rw-r--r--spec/services/personal_access_tokens/last_used_service_spec.rb2
-rw-r--r--spec/services/personal_access_tokens/revoke_service_spec.rb2
-rw-r--r--spec/services/post_receive_service_spec.rb2
-rw-r--r--spec/services/preview_markdown_service_spec.rb2
-rw-r--r--spec/services/product_analytics/build_activity_graph_service_spec.rb2
-rw-r--r--spec/services/product_analytics/build_graph_service_spec.rb2
-rw-r--r--spec/services/projects/after_rename_service_spec.rb2
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb2
-rw-r--r--spec/services/projects/all_issues_count_service_spec.rb2
-rw-r--r--spec/services/projects/all_merge_requests_count_service_spec.rb2
-rw-r--r--spec/services/projects/android_target_platform_detector_service_spec.rb2
-rw-r--r--spec/services/projects/apple_target_platform_detector_service_spec.rb2
-rw-r--r--spec/services/projects/auto_devops/disable_service_spec.rb2
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb2
-rw-r--r--spec/services/projects/batch_open_issues_count_service_spec.rb2
-rw-r--r--spec/services/projects/batch_open_merge_requests_count_service_spec.rb32
-rw-r--r--spec/services/projects/blame_service_spec.rb2
-rw-r--r--spec/services/projects/branches_by_mode_service_spec.rb2
-rw-r--r--spec/services/projects/cleanup_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb7
-rw-r--r--spec/services/projects/container_repository/delete_tags_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb20
-rw-r--r--spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb2
-rw-r--r--spec/services/projects/count_service_spec.rb2
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb2
-rw-r--r--spec/services/projects/deploy_tokens/create_service_spec.rb2
-rw-r--r--spec/services/projects/deploy_tokens/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/detect_repository_languages_service_spec.rb2
-rw-r--r--spec/services/projects/download_service_spec.rb2
-rw-r--r--spec/services/projects/enable_deploy_key_service_spec.rb2
-rw-r--r--spec/services/projects/fetch_statistics_increment_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/forks/sync_service_spec.rb185
-rw-r--r--spec/services/projects/forks_count_service_spec.rb2
-rw-r--r--spec/services/projects/git_deduplication_service_spec.rb2
-rw-r--r--spec/services/projects/gitlab_projects_import_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/update_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/base_attachment_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migration_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/rollback_service_spec.rb2
-rw-r--r--spec/services/projects/import_error_filter_spec.rb2
-rw-r--r--spec/services/projects/import_export/relation_export_service_spec.rb4
-rw-r--r--spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_import_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb21
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb2
-rw-r--r--spec/services/projects/move_access_service_spec.rb2
-rw-r--r--spec/services/projects/move_deploy_keys_projects_service_spec.rb2
-rw-r--r--spec/services/projects/move_forks_service_spec.rb2
-rw-r--r--spec/services/projects/move_lfs_objects_projects_service_spec.rb2
-rw-r--r--spec/services/projects/move_notification_settings_service_spec.rb2
-rw-r--r--spec/services/projects/move_project_authorizations_service_spec.rb2
-rw-r--r--spec/services/projects/move_project_group_links_service_spec.rb2
-rw-r--r--spec/services/projects/move_project_members_service_spec.rb2
-rw-r--r--spec/services/projects/move_users_star_projects_service_spec.rb2
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb2
-rw-r--r--spec/services/projects/open_merge_requests_count_service_spec.rb2
-rw-r--r--spec/services/projects/operations/update_service_spec.rb2
-rw-r--r--spec/services/projects/overwrite_project_service_spec.rb2
-rw-r--r--spec/services/projects/participants_service_spec.rb2
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb2
-rw-r--r--spec/services/projects/prometheus/metrics/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/protect_default_branch_service_spec.rb2
-rw-r--r--spec/services/projects/readme_renderer_service_spec.rb2
-rw-r--r--spec/services/projects/record_target_platforms_service_spec.rb2
-rw-r--r--spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb2
-rw-r--r--spec/services/projects/repository_languages_service_spec.rb2
-rw-r--r--spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_service_spec.rb2
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb2
-rw-r--r--spec/services/projects/update_repository_storage_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb154
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb2
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb2
-rw-r--r--spec/services/prometheus/proxy_variable_substitution_service_spec.rb2
-rw-r--r--spec/services/protected_branches/api_service_spec.rb2
-rw-r--r--spec/services/protected_branches/cache_service_spec.rb2
-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/protected_tags/create_service_spec.rb2
-rw-r--r--spec/services/protected_tags/destroy_service_spec.rb2
-rw-r--r--spec/services/protected_tags/update_service_spec.rb2
-rw-r--r--spec/services/push_event_payload_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb4
-rw-r--r--spec/services/quick_actions/target_service_spec.rb2
-rw-r--r--spec/services/releases/create_evidence_service_spec.rb2
-rw-r--r--spec/services/releases/destroy_service_spec.rb2
-rw-r--r--spec/services/releases/links/create_service_spec.rb84
-rw-r--r--spec/services/releases/links/destroy_service_spec.rb72
-rw-r--r--spec/services/releases/links/update_service_spec.rb89
-rw-r--r--spec/services/repositories/changelog_service_spec.rb2
-rw-r--r--spec/services/repositories/destroy_service_spec.rb2
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb2
-rw-r--r--spec/services/reset_project_cache_service_spec.rb2
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb56
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb2
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb2
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb2
-rw-r--r--spec/services/resource_events/change_state_service_spec.rb2
-rw-r--r--spec/services/resource_events/merge_into_notes_service_spec.rb2
-rw-r--r--spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb2
-rw-r--r--spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb2
-rw-r--r--spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb2
-rw-r--r--spec/services/search/global_service_spec.rb2
-rw-r--r--spec/services/search/group_service_spec.rb2
-rw-r--r--spec/services/search/snippet_service_spec.rb2
-rw-r--r--spec/services/security/ci_configuration/container_scanning_create_service_spec.rb2
-rw-r--r--spec/services/security/ci_configuration/sast_iac_create_service_spec.rb2
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb2
-rw-r--r--spec/services/security/ci_configuration/secret_detection_create_service_spec.rb2
-rw-r--r--spec/services/security/merge_reports_service_spec.rb2
-rw-r--r--spec/services/serverless/associate_domain_service_spec.rb91
-rw-r--r--spec/services/service_desk_settings/update_service_spec.rb2
-rw-r--r--spec/services/service_ping/submit_service_ping_service_spec.rb2
-rw-r--r--spec/services/service_response_spec.rb2
-rw-r--r--spec/services/snippets/bulk_destroy_service_spec.rb2
-rw-r--r--spec/services/snippets/count_service_spec.rb2
-rw-r--r--spec/services/snippets/create_service_spec.rb2
-rw-r--r--spec/services/snippets/destroy_service_spec.rb2
-rw-r--r--spec/services/snippets/repository_validation_service_spec.rb2
-rw-r--r--spec/services/snippets/schedule_bulk_repository_shard_moves_service_spec.rb2
-rw-r--r--spec/services/snippets/update_repository_storage_service_spec.rb2
-rw-r--r--spec/services/snippets/update_service_spec.rb2
-rw-r--r--spec/services/snippets/update_statistics_service_spec.rb2
-rw-r--r--spec/services/spam/akismet_mark_as_spam_service_spec.rb2
-rw-r--r--spec/services/spam/akismet_service_spec.rb2
-rw-r--r--spec/services/spam/ham_service_spec.rb2
-rw-r--r--spec/services/spam/spam_action_service_spec.rb2
-rw-r--r--spec/services/spam/spam_params_spec.rb2
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb2
-rw-r--r--spec/services/submodules/update_service_spec.rb2
-rw-r--r--spec/services/suggestions/apply_service_spec.rb2
-rw-r--r--spec/services/suggestions/create_service_spec.rb2
-rw-r--r--spec/services/suggestions/outdate_service_spec.rb2
-rw-r--r--spec/services/system_hooks_service_spec.rb2
-rw-r--r--spec/services/system_notes/alert_management_service_spec.rb2
-rw-r--r--spec/services/system_notes/base_service_spec.rb2
-rw-r--r--spec/services/system_notes/commit_service_spec.rb82
-rw-r--r--spec/services/system_notes/design_management_service_spec.rb2
-rw-r--r--spec/services/system_notes/incident_service_spec.rb2
-rw-r--r--spec/services/system_notes/incidents_service_spec.rb2
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb2
-rw-r--r--spec/services/system_notes/merge_requests_service_spec.rb2
-rw-r--r--spec/services/system_notes/time_tracking_service_spec.rb2
-rw-r--r--spec/services/system_notes/zoom_service_spec.rb2
-rw-r--r--spec/services/tags/create_service_spec.rb2
-rw-r--r--spec/services/tags/destroy_service_spec.rb2
-rw-r--r--spec/services/task_list_toggle_service_spec.rb2
-rw-r--r--spec/services/tasks_to_be_done/base_service_spec.rb2
-rw-r--r--spec/services/terraform/remote_state_handler_spec.rb2
-rw-r--r--spec/services/terraform/states/destroy_service_spec.rb2
-rw-r--r--spec/services/terraform/states/trigger_destroy_service_spec.rb2
-rw-r--r--spec/services/test_hooks/project_service_spec.rb2
-rw-r--r--spec/services/test_hooks/system_service_spec.rb2
-rw-r--r--spec/services/timelogs/delete_service_spec.rb2
-rw-r--r--spec/services/todo_service_spec.rb3
-rw-r--r--spec/services/todos/allowed_target_filter_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/confidential_issue_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/design_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/destroyed_issuable_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/project_private_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/unauthorized_features_service_spec.rb2
-rw-r--r--spec/services/topics/merge_service_spec.rb2
-rw-r--r--spec/services/two_factor/destroy_service_spec.rb2
-rw-r--r--spec/services/update_container_registry_info_service_spec.rb2
-rw-r--r--spec/services/update_merge_request_metrics_service_spec.rb2
-rw-r--r--spec/services/upload_service_spec.rb2
-rw-r--r--spec/services/uploads/destroy_service_spec.rb2
-rw-r--r--spec/services/user_preferences/update_service_spec.rb2
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb2
-rw-r--r--spec/services/users/activity_service_spec.rb2
-rw-r--r--spec/services/users/approve_service_spec.rb2
-rw-r--r--spec/services/users/authorized_build_service_spec.rb2
-rw-r--r--spec/services/users/ban_service_spec.rb2
-rw-r--r--spec/services/users/banned_user_base_service_spec.rb2
-rw-r--r--spec/services/users/batch_status_cleaner_service_spec.rb2
-rw-r--r--spec/services/users/block_service_spec.rb2
-rw-r--r--spec/services/users/build_service_spec.rb2
-rw-r--r--spec/services/users/create_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb2
-rw-r--r--spec/services/users/dismiss_callout_service_spec.rb2
-rw-r--r--spec/services/users/dismiss_group_callout_service_spec.rb2
-rw-r--r--spec/services/users/dismiss_project_callout_service_spec.rb2
-rw-r--r--spec/services/users/email_verification/generate_token_service_spec.rb2
-rw-r--r--spec/services/users/email_verification/validate_token_service_spec.rb2
-rw-r--r--spec/services/users/in_product_marketing_email_records_spec.rb2
-rw-r--r--spec/services/users/keys_count_service_spec.rb2
-rw-r--r--spec/services/users/last_push_event_service_spec.rb2
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb2
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_service_spec.rb2
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb2
-rw-r--r--spec/services/users/registrations_build_service_spec.rb2
-rw-r--r--spec/services/users/reject_service_spec.rb2
-rw-r--r--spec/services/users/repair_ldap_blocked_service_spec.rb2
-rw-r--r--spec/services/users/respond_to_terms_service_spec.rb2
-rw-r--r--spec/services/users/saved_replies/create_service_spec.rb2
-rw-r--r--spec/services/users/saved_replies/destroy_service_spec.rb2
-rw-r--r--spec/services/users/saved_replies/update_service_spec.rb2
-rw-r--r--spec/services/users/set_status_service_spec.rb2
-rw-r--r--spec/services/users/signup_service_spec.rb8
-rw-r--r--spec/services/users/unban_service_spec.rb2
-rw-r--r--spec/services/users/unblock_service_spec.rb2
-rw-r--r--spec/services/users/update_canonical_email_service_spec.rb2
-rw-r--r--spec/services/users/update_highest_member_role_service_spec.rb2
-rw-r--r--spec/services/users/update_service_spec.rb2
-rw-r--r--spec/services/users/update_todo_count_cache_service_spec.rb2
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb2
-rw-r--r--spec/services/users/validate_manual_otp_service_spec.rb33
-rw-r--r--spec/services/users/validate_push_otp_service_spec.rb2
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb2
-rw-r--r--spec/services/web_hooks/destroy_service_spec.rb2
-rw-r--r--spec/services/web_hooks/log_destroy_service_spec.rb2
-rw-r--r--spec/services/web_hooks/log_execution_service_spec.rb2
-rw-r--r--spec/services/webauthn/authenticate_service_spec.rb2
-rw-r--r--spec/services/webauthn/register_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/base_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/event_create_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb2
-rw-r--r--spec/services/wikis/create_attachment_service_spec.rb2
-rw-r--r--spec/services/work_items/build_service_spec.rb2
-rw-r--r--spec/services/work_items/create_from_task_service_spec.rb2
-rw-r--r--spec/services/work_items/create_service_spec.rb2
-rw-r--r--spec/services/work_items/delete_service_spec.rb2
-rw-r--r--spec/services/work_items/delete_task_service_spec.rb2
-rw-r--r--spec/services/work_items/export_csv_service_spec.rb5
-rw-r--r--spec/services/work_items/import_csv_service_spec.rb122
-rw-r--r--spec/services/work_items/parent_links/create_service_spec.rb34
-rw-r--r--spec/services/work_items/parent_links/destroy_service_spec.rb2
-rw-r--r--spec/services/work_items/task_list_reference_removal_service_spec.rb2
-rw-r--r--spec/services/work_items/task_list_reference_replacement_service_spec.rb2
-rw-r--r--spec/services/work_items/update_service_spec.rb2
-rw-r--r--spec/services/work_items/widgets/assignees_service/update_service_spec.rb2
-rw-r--r--spec/services/work_items/widgets/description_service/update_service_spec.rb2
-rw-r--r--spec/services/work_items/widgets/hierarchy_service/create_service_spec.rb31
-rw-r--r--spec/services/work_items/widgets/milestone_service/create_service_spec.rb2
-rw-r--r--spec/services/work_items/widgets/milestone_service/update_service_spec.rb2
-rw-r--r--spec/services/work_items/widgets/notifications_service/update_service_spec.rb117
-rw-r--r--spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb2
-rw-r--r--spec/services/x509_certificate_revoke_service_spec.rb2
-rw-r--r--spec/simplecov_env.rb1
-rw-r--r--spec/spec_helper.rb40
-rw-r--r--spec/support/ability_check.rb73
-rw-r--r--spec/support/ability_check_todo.yml73
-rw-r--r--spec/support/helpers/ci/template_helpers.rb4
-rw-r--r--spec/support/helpers/content_editor_helpers.rb58
-rw-r--r--spec/support/helpers/fake_u2f_device.rb47
-rw-r--r--spec/support/helpers/features/two_factor_helpers.rb41
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb11
-rw-r--r--spec/support/helpers/fixture_helpers.rb2
-rw-r--r--spec/support/helpers/gitaly_setup.rb17
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/helpers/login_helpers.rb14
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb8
-rw-r--r--spec/support/helpers/query_recorder.rb4
-rw-r--r--spec/support/helpers/repo_helpers.rb4
-rw-r--r--spec/support/helpers/stub_configuration.rb2
-rw-r--r--spec/support/helpers/stub_object_storage.rb4
-rw-r--r--spec/support/helpers/test_env.rb1
-rw-r--r--spec/support/helpers/usage_data_helpers.rb2
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb14
-rw-r--r--spec/support/matchers/exceed_redis_call_limit.rb57
-rw-r--r--spec/support/matchers/request_urgency_matcher.rb29
-rw-r--r--spec/support/permissions_check.rb18
-rw-r--r--spec/support/protected_tags/access_control_ce_shared_examples.rb27
-rw-r--r--spec/support/rspec.rb7
-rw-r--r--spec/support/rspec_order_todo.yml12
-rw-r--r--spec/support/services/import_csv_service_shared_examples.rb38
-rw-r--r--spec/support/services/issuable_import_csv_service_shared_examples.rb37
-rw-r--r--spec/support/shared_contexts/features/integrations/integrations_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb52
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb15
-rw-r--r--spec/support/shared_contexts/rack_attack_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb4
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb464
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb127
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/abuse_report_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb97
-rw-r--r--spec/support/shared_examples/features/incident_details_routing_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb61
-rw-r--r--spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/trial_email_validation_shared_example.rb67
-rw-r--r--spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb66
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb123
-rw-r--r--spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb154
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/helpers/callouts_for_web_hooks.rb49
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb1
-rw-r--r--spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb131
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb104
-rw-r--r--spec/support/shared_examples/lib/menus_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/mailers/export_csv_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/models/active_record_enum_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/ci/token_format_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb98
-rw-r--r--spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb16
-rw-r--r--spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/observability/csp_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/observability/embed_observabilities_examples.rb61
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/admin_mode_shared_examples.rb71
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/requests/api/discussions_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/hooks_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb455
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/applications_controller_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb141
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb1
-rw-r--r--spec/support/stub_dot_com_check.rb20
-rw-r--r--spec/support_specs/ability_check_spec.rb148
-rw-r--r--spec/support_specs/helpers/packages/npm_spec.rb133
-rw-r--r--spec/support_specs/matchers/exceed_redis_call_limit_spec.rb59
-rw-r--r--spec/tasks/gitlab/db/decomposition/connection_status_spec.rb61
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb145
-rw-r--r--spec/tasks/gitlab/feature_categories_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb2
-rw-r--r--spec/tooling/danger/sidekiq_args_spec.rb125
-rw-r--r--spec/tooling/danger/specs_spec.rb10
-rw-r--r--spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb14
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb17
-rw-r--r--spec/uploaders/object_storage/cdn_spec.rb88
-rw-r--r--spec/uploaders/object_storage_spec.rb2
-rw-r--r--spec/validators/addressable_url_validator_spec.rb70
-rw-r--r--spec/views/admin/application_settings/ci_cd.html.haml_spec.rb5
-rw-r--r--spec/views/admin/application_settings/network.html.haml_spec.rb33
-rw-r--r--spec/views/admin/sessions/two_factor.html.haml_spec.rb10
-rw-r--r--spec/views/devise/confirmations/almost_there.html.haml_spec.rb17
-rw-r--r--spec/views/devise/sessions/new.html.haml_spec.rb3
-rw-r--r--spec/views/devise/shared/_error_messages.html.haml_spec.rb43
-rw-r--r--spec/views/devise/shared/_signup_box.html.haml_spec.rb1
-rw-r--r--spec/views/events/event/_common.html.haml_spec.rb13
-rw-r--r--spec/views/groups/group_members/index.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/group.html.haml_spec.rb30
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb27
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb14
-rw-r--r--spec/views/layouts/project.html.haml_spec.rb29
-rw-r--r--spec/views/notify/import_issues_csv_email.html.haml_spec.rb4
-rw-r--r--spec/views/notify/import_work_items_csv_email.html.haml_spec.rb133
-rw-r--r--spec/views/profiles/keys/_key.html.haml_spec.rb21
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb24
-rw-r--r--spec/views/projects/empty.html.haml_spec.rb4
-rw-r--r--spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb4
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb19
-rw-r--r--spec/views/projects/project_members/index.html.haml_spec.rb1
-rw-r--r--spec/workers/admin_email_worker_spec.rb2
-rw-r--r--spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb2
-rw-r--r--spec/workers/analytics/usage_trends/counter_job_worker_spec.rb2
-rw-r--r--spec/workers/approve_blocked_pending_approval_users_worker_spec.rb2
-rw-r--r--spec/workers/authorized_keys_worker_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/project_recalculate_worker_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb2
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb2
-rw-r--r--spec/workers/auto_devops/disable_worker_spec.rb2
-rw-r--r--spec/workers/auto_merge_process_worker_spec.rb2
-rw-r--r--spec/workers/background_migration/ci_database_worker_spec.rb2
-rw-r--r--spec/workers/background_migration_worker_spec.rb2
-rw-r--r--spec/workers/build_hooks_worker_spec.rb2
-rw-r--r--spec/workers/build_queue_worker_spec.rb2
-rw-r--r--spec/workers/build_success_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/relation_export_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/stuck_import_worker_spec.rb2
-rw-r--r--spec/workers/chat_notification_worker_spec.rb2
-rw-r--r--spec/workers/ci/archive_trace_worker_spec.rb2
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb14
-rw-r--r--spec/workers/ci/build_finished_worker_spec.rb2
-rw-r--r--spec/workers/ci/build_prepare_worker_spec.rb2
-rw-r--r--spec/workers/ci/build_schedule_worker_spec.rb2
-rw-r--r--spec/workers/ci/build_trace_chunk_flush_worker_spec.rb2
-rw-r--r--spec/workers/ci/cancel_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/ci/create_cross_project_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/ci/create_downstream_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/ci/daily_build_group_report_results_worker_spec.rb2
-rw-r--r--spec/workers/ci/delete_objects_worker_spec.rb2
-rw-r--r--spec/workers/ci/delete_unit_tests_worker_spec.rb2
-rw-r--r--spec/workers/ci/drop_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb2
-rw-r--r--spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb2
-rw-r--r--spec/workers/ci/parse_secure_file_metadata_worker_spec.rb2
-rw-r--r--spec/workers/ci/pending_builds/update_group_worker_spec.rb2
-rw-r--r--spec/workers/ci/pending_builds/update_project_worker_spec.rb2
-rw-r--r--spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb2
-rw-r--r--spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb2
-rw-r--r--spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/ci/pipeline_bridge_status_worker_spec.rb2
-rw-r--r--spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb2
-rw-r--r--spec/workers/ci/retry_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/ci/schedule_delete_objects_cron_worker_spec.rb2
-rw-r--r--spec/workers/ci/stuck_builds/drop_running_worker_spec.rb2
-rw-r--r--spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb2
-rw-r--r--spec/workers/ci/test_failure_history_worker_spec.rb2
-rw-r--r--spec/workers/ci/track_failed_build_worker_spec.rb2
-rw-r--r--spec/workers/ci/update_locked_unknown_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/ci_platform_metrics_update_cron_worker_spec.rb2
-rw-r--r--spec/workers/cleanup_container_repository_worker_spec.rb2
-rw-r--r--spec/workers/clusters/agents/delete_expired_events_worker_spec.rb2
-rw-r--r--spec/workers/clusters/applications/activate_integration_worker_spec.rb2
-rw-r--r--spec/workers/clusters/applications/deactivate_integration_worker_spec.rb2
-rw-r--r--spec/workers/clusters/cleanup/project_namespace_worker_spec.rb2
-rw-r--r--spec/workers/clusters/cleanup/service_account_worker_spec.rb2
-rw-r--r--spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb2
-rw-r--r--spec/workers/concerns/application_worker_spec.rb2
-rw-r--r--spec/workers/concerns/cluster_agent_queue_spec.rb3
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb21
-rw-r--r--spec/workers/concerns/cronjob_queue_spec.rb6
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb86
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb18
-rw-r--r--spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/notify_upon_death_spec.rb2
-rw-r--r--spec/workers/concerns/limited_capacity/job_tracker_spec.rb2
-rw-r--r--spec/workers/concerns/limited_capacity/worker_spec.rb2
-rw-r--r--spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb2
-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/project_import_options_spec.rb2
-rw-r--r--spec/workers/concerns/reenqueuer_spec.rb2
-rw-r--r--spec/workers/concerns/repository_check_queue_spec.rb6
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb2
-rw-r--r--spec/workers/concerns/worker_attributes_spec.rb2
-rw-r--r--spec/workers/concerns/worker_context_spec.rb2
-rw-r--r--spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb30
-rw-r--r--spec/workers/container_expiration_policy_worker_spec.rb12
-rw-r--r--spec/workers/container_registry/cleanup_worker_spec.rb2
-rw-r--r--spec/workers/container_registry/delete_container_repository_worker_spec.rb2
-rw-r--r--spec/workers/container_registry/migration/enqueuer_worker_spec.rb3
-rw-r--r--spec/workers/container_registry/migration/guard_worker_spec.rb2
-rw-r--r--spec/workers/container_registry/migration/observer_worker_spec.rb2
-rw-r--r--spec/workers/counters/cleanup_refresh_worker_spec.rb2
-rw-r--r--spec/workers/create_commit_signature_worker_spec.rb2
-rw-r--r--spec/workers/create_note_diff_file_worker_spec.rb2
-rw-r--r--spec/workers/create_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/database/batched_background_migration/ci_database_worker_spec.rb2
-rw-r--r--spec/workers/database/batched_background_migration_worker_spec.rb2
-rw-r--r--spec/workers/database/ci_namespace_mirrors_consistency_check_worker_spec.rb2
-rw-r--r--spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb2
-rw-r--r--spec/workers/database/drop_detached_partitions_worker_spec.rb2
-rw-r--r--spec/workers/database/partition_management_worker_spec.rb2
-rw-r--r--spec/workers/delete_container_repository_worker_spec.rb2
-rw-r--r--spec/workers/delete_diff_files_worker_spec.rb2
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb2
-rw-r--r--spec/workers/delete_user_worker_spec.rb2
-rw-r--r--spec/workers/dependency_proxy/cleanup_blob_worker_spec.rb2
-rw-r--r--spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb2
-rw-r--r--spec/workers/dependency_proxy/cleanup_manifest_worker_spec.rb2
-rw-r--r--spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb2
-rw-r--r--spec/workers/deployments/archive_in_project_worker_spec.rb2
-rw-r--r--spec/workers/deployments/drop_older_deployments_worker_spec.rb2
-rw-r--r--spec/workers/deployments/hooks_worker_spec.rb2
-rw-r--r--spec/workers/deployments/link_merge_request_worker_spec.rb2
-rw-r--r--spec/workers/deployments/update_environment_worker_spec.rb2
-rw-r--r--spec/workers/design_management/copy_design_collection_worker_spec.rb2
-rw-r--r--spec/workers/design_management/new_version_worker_spec.rb2
-rw-r--r--spec/workers/destroy_pages_deployments_worker_spec.rb2
-rw-r--r--spec/workers/detect_repository_languages_worker_spec.rb2
-rw-r--r--spec/workers/disallow_two_factor_for_group_worker_spec.rb2
-rw-r--r--spec/workers/disallow_two_factor_for_subgroups_worker_spec.rb2
-rw-r--r--spec/workers/email_receiver_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb10
-rw-r--r--spec/workers/environments/auto_delete_cron_worker_spec.rb2
-rw-r--r--spec/workers/environments/auto_stop_cron_worker_spec.rb2
-rw-r--r--spec/workers/environments/auto_stop_worker_spec.rb2
-rw-r--r--spec/workers/environments/canary_ingress/update_worker_spec.rb2
-rw-r--r--spec/workers/error_tracking_issue_link_worker_spec.rb2
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb4
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/export_csv_worker_spec.rb2
-rw-r--r--spec/workers/external_service_reactive_caching_worker_spec.rb2
-rw-r--r--spec/workers/file_hook_worker_spec.rb2
-rw-r--r--spec/workers/flush_counter_increments_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/advance_stage_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/attachments/import_issue_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/attachments/import_merge_request_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/attachments/import_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/attachments/import_release_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_collaborator_worker_spec.rb38
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb73
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb2
-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.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb10
-rw-r--r--spec/workers/gitlab/import/stuck_import_job_spec.rb2
-rw-r--r--spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/import_issue_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/phabricator_import/base_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb2
-rw-r--r--spec/workers/gitlab_performance_bar_stats_worker_spec.rb2
-rw-r--r--spec/workers/gitlab_service_ping_worker_spec.rb8
-rw-r--r--spec/workers/gitlab_shell_worker_spec.rb2
-rw-r--r--spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb2
-rw-r--r--spec/workers/group_destroy_worker_spec.rb23
-rw-r--r--spec/workers/group_export_worker_spec.rb2
-rw-r--r--spec/workers/group_import_worker_spec.rb2
-rw-r--r--spec/workers/groups/update_statistics_worker_spec.rb2
-rw-r--r--spec/workers/groups/update_two_factor_requirement_for_members_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/migrator_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/project_migrate_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/project_rollback_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/rollbacker_worker_spec.rb2
-rw-r--r--spec/workers/import_issues_csv_worker_spec.rb2
-rw-r--r--spec/workers/incident_management/add_severity_system_note_worker_spec.rb2
-rw-r--r--spec/workers/incident_management/close_incident_worker_spec.rb2
-rw-r--r--spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb2
-rw-r--r--spec/workers/incident_management/process_alert_worker_v2_spec.rb2
-rw-r--r--spec/workers/integrations/create_external_cross_reference_worker_spec.rb2
-rw-r--r--spec/workers/integrations/execute_worker_spec.rb2
-rw-r--r--spec/workers/integrations/irker_worker_spec.rb2
-rw-r--r--spec/workers/invalid_gpg_signature_update_worker_spec.rb2
-rw-r--r--spec/workers/issuable/label_links_destroy_worker_spec.rb2
-rw-r--r--spec/workers/issuable_export_csv_worker_spec.rb45
-rw-r--r--spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb2
-rw-r--r--spec/workers/issue_due_scheduler_worker_spec.rb2
-rw-r--r--spec/workers/issues/close_worker_spec.rb2
-rw-r--r--spec/workers/issues/placement_worker_spec.rb2
-rw-r--r--spec/workers/issues/rebalancing_worker_spec.rb2
-rw-r--r--spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/forward_event_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/retry_request_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_branch_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_builds_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_deployments_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_feature_flags_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_merge_request_worker_spec.rb2
-rw-r--r--spec/workers/jira_connect/sync_project_worker_spec.rb2
-rw-r--r--spec/workers/loose_foreign_keys/cleanup_worker_spec.rb2
-rw-r--r--spec/workers/mail_scheduler/issue_due_worker_spec.rb2
-rw-r--r--spec/workers/mail_scheduler/notification_service_worker_spec.rb2
-rw-r--r--spec/workers/member_invitation_reminder_emails_worker_spec.rb2
-rw-r--r--spec/workers/members_destroyer/unassign_issuables_worker_spec.rb2
-rw-r--r--spec/workers/merge_request_cleanup_refs_worker_spec.rb8
-rw-r--r--spec/workers/merge_request_mergeability_check_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/close_issue_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/create_approval_event_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/create_approval_note_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/delete_source_branch_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/handle_assignees_change_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/resolve_todos_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/update_head_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb2
-rw-r--r--spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb2
-rw-r--r--spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb2
-rw-r--r--spec/workers/migrate_external_diffs_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/process_sync_events_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/root_statistics_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/schedule_aggregation_worker_spec.rb2
-rw-r--r--spec/workers/namespaces/update_root_statistics_worker_spec.rb2
-rw-r--r--spec/workers/new_issue_worker_spec.rb2
-rw-r--r--spec/workers/new_merge_request_worker_spec.rb12
-rw-r--r--spec/workers/new_note_worker_spec.rb2
-rw-r--r--spec/workers/object_pool/create_worker_spec.rb2
-rw-r--r--spec/workers/object_pool/destroy_worker_spec.rb2
-rw-r--r--spec/workers/object_pool/join_worker_spec.rb2
-rw-r--r--spec/workers/onboarding/issue_created_worker_spec.rb2
-rw-r--r--spec/workers/onboarding/pipeline_created_worker_spec.rb2
-rw-r--r--spec/workers/onboarding/progress_worker_spec.rb2
-rw-r--r--spec/workers/onboarding/user_added_worker_spec.rb2
-rw-r--r--spec/workers/packages/cleanup/execute_policy_worker_spec.rb2
-rw-r--r--spec/workers/packages/cleanup_package_file_worker_spec.rb2
-rw-r--r--spec/workers/packages/cleanup_package_registry_worker_spec.rb2
-rw-r--r--spec/workers/packages/composer/cache_cleanup_worker_spec.rb2
-rw-r--r--spec/workers/packages/composer/cache_update_worker_spec.rb2
-rw-r--r--spec/workers/packages/debian/generate_distribution_worker_spec.rb10
-rw-r--r--spec/workers/packages/debian/process_changes_worker_spec.rb6
-rw-r--r--spec/workers/packages/debian/process_package_file_worker_spec.rb1
-rw-r--r--spec/workers/packages/go/sync_packages_worker_spec.rb2
-rw-r--r--spec/workers/packages/helm/extraction_worker_spec.rb2
-rw-r--r--spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb2
-rw-r--r--spec/workers/packages/nuget/extraction_worker_spec.rb2
-rw-r--r--spec/workers/packages/rubygems/extraction_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_removal_cron_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_ssl_renewal_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_verification_cron_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_verification_worker_spec.rb2
-rw-r--r--spec/workers/pages_worker_spec.rb2
-rw-r--r--spec/workers/partition_creation_worker_spec.rb2
-rw-r--r--spec/workers/personal_access_tokens/expired_notification_worker_spec.rb2
-rw-r--r--spec/workers/personal_access_tokens/expiring_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_hooks_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_process_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb3
-rw-r--r--spec/workers/process_commit_worker_spec.rb2
-rw-r--r--spec/workers/project_cache_worker_spec.rb2
-rw-r--r--spec/workers/project_destroy_worker_spec.rb25
-rw-r--r--spec/workers/project_export_worker_spec.rb2
-rw-r--r--spec/workers/projects/after_import_worker_spec.rb2
-rw-r--r--spec/workers/projects/finalize_project_statistics_refresh_worker_spec.rb2
-rw-r--r--spec/workers/projects/import_export/create_relation_exports_worker_spec.rb67
-rw-r--r--spec/workers/projects/import_export/relation_export_worker_spec.rb49
-rw-r--r--spec/workers/projects/import_export/wait_relation_exports_worker_spec.rb123
-rw-r--r--spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb2
-rw-r--r--spec/workers/projects/inactive_projects_deletion_notification_worker_spec.rb2
-rw-r--r--spec/workers/projects/post_creation_worker_spec.rb2
-rw-r--r--spec/workers/projects/process_sync_events_worker_spec.rb2
-rw-r--r--spec/workers/projects/record_target_platforms_worker_spec.rb2
-rw-r--r--spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb2
-rw-r--r--spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb2
-rw-r--r--spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb2
-rw-r--r--spec/workers/projects/update_repository_storage_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_group_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_inherit_descendant_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_inherit_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_worker_spec.rb2
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb14
-rw-r--r--spec/workers/purge_dependency_proxy_cache_worker_spec.rb2
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb2
-rw-r--r--spec/workers/rebase_worker_spec.rb2
-rw-r--r--spec/workers/releases/create_evidence_worker_spec.rb2
-rw-r--r--spec/workers/releases/manage_evidence_worker_spec.rb2
-rw-r--r--spec/workers/remote_mirror_notification_worker_spec.rb2
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb2
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb2
-rw-r--r--spec/workers/remove_unaccepted_member_invites_worker_spec.rb2
-rw-r--r--spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/clear_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb2
-rw-r--r--spec/workers/repository_cleanup_worker_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb2
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb2
-rw-r--r--spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb2
-rw-r--r--spec/workers/schedule_migrate_external_diffs_worker_spec.rb2
-rw-r--r--spec/workers/self_monitoring_project_create_worker_spec.rb2
-rw-r--r--spec/workers/self_monitoring_project_delete_worker_spec.rb2
-rw-r--r--spec/workers/service_desk_email_receiver_worker_spec.rb2
-rw-r--r--spec/workers/snippets/schedule_bulk_repository_shard_moves_worker_spec.rb2
-rw-r--r--spec/workers/snippets/update_repository_storage_worker_spec.rb2
-rw-r--r--spec/workers/ssh_keys/expired_notification_worker_spec.rb3
-rw-r--r--spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb3
-rw-r--r--spec/workers/stage_update_worker_spec.rb2
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb2
-rw-r--r--spec/workers/stuck_export_jobs_worker_spec.rb2
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb2
-rw-r--r--spec/workers/system_hook_push_worker_spec.rb2
-rw-r--r--spec/workers/tasks_to_be_done/create_worker_spec.rb2
-rw-r--r--spec/workers/terraform/states/destroy_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/confidential_issue_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/destroyed_designs_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/destroyed_issuable_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/entity_leave_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/group_private_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/private_features_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/project_private_worker_spec.rb2
-rw-r--r--spec/workers/trending_projects_worker_spec.rb2
-rw-r--r--spec/workers/update_container_registry_info_worker_spec.rb2
-rw-r--r--spec/workers/update_external_pull_requests_worker_spec.rb2
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb2
-rw-r--r--spec/workers/update_highest_role_worker_spec.rb2
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb2
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb2
-rw-r--r--spec/workers/upload_checksum_worker_spec.rb2
-rw-r--r--spec/workers/user_status_cleanup/batch_worker_spec.rb2
-rw-r--r--spec/workers/users/create_statistics_worker_spec.rb2
-rw-r--r--spec/workers/users/deactivate_dormant_users_worker_spec.rb5
-rw-r--r--spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb2
-rw-r--r--spec/workers/web_hook_worker_spec.rb2
-rw-r--r--spec/workers/web_hooks/log_destroy_worker_spec.rb2
-rw-r--r--spec/workers/x509_certificate_revoke_worker_spec.rb2
-rw-r--r--spec/workers/x509_issuer_crl_check_worker_spec.rb2
4001 files changed, 52525 insertions, 25546 deletions
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 0c32fa2571a..428a0588bdd 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -1,13 +1,12 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'rspec-parameterized'
+require 'spec_helper'
require_relative '../../support/stub_settings_source'
require_relative '../../../sidekiq_cluster/cli'
require_relative '../../support/helpers/next_instance_of'
-RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubocop:disable RSpec/FilePath
+RSpec.describe Gitlab::SidekiqCluster::CLI, feature_category: :gitlab_cli, stub_settings_source: true do # rubocop:disable RSpec/FilePath
include NextInstanceOf
let(:cli) { described_class.new('/dev/null') }
diff --git a/spec/config/inject_enterprise_edition_module_spec.rb b/spec/config/inject_enterprise_edition_module_spec.rb
index e8c0905ff89..6689fed02f5 100644
--- a/spec/config/inject_enterprise_edition_module_spec.rb
+++ b/spec/config/inject_enterprise_edition_module_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe InjectEnterpriseEditionModule, feature_category: :not_owned do
+RSpec.describe InjectEnterpriseEditionModule, feature_category: :shared do
let(:extension_name) { 'FF' }
let(:extension_namespace) { Module.new }
let(:fish_name) { 'Fish' }
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index 7b4fa495288..025966c8940 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('config', 'object_store_settings.rb')
-RSpec.describe ObjectStoreSettings, feature_category: :not_owned do
+RSpec.describe ObjectStoreSettings, feature_category: :shared do
describe '#parse!' do
let(:settings) { Settingslogic.new(config) }
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index 0928f2b72ff..b464a4eee8b 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Settings, feature_category: :authentication_and_authorization do
+RSpec.describe Settings, feature_category: :system_access do
using RSpec::Parameterized::TableSyntax
describe 'omniauth' do
@@ -198,4 +198,18 @@ RSpec.describe Settings, feature_category: :authentication_and_authorization do
end
end
end
+
+ describe '.build_sidekiq_routing_rules' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:input_rules, :result) do
+ nil | [['*', nil]]
+ [] | [['*', nil]]
+ [['name=foobar', 'foobar']] | [['name=foobar', 'foobar']]
+ end
+
+ with_them do
+ it { expect(described_class.send(:build_sidekiq_routing_rules, input_rules)).to eq(result) }
+ end
+ end
end
diff --git a/spec/config/smime_signature_settings_spec.rb b/spec/config/smime_signature_settings_spec.rb
index 477ad4a74ed..73dca66c666 100644
--- a/spec/config/smime_signature_settings_spec.rb
+++ b/spec/config/smime_signature_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SmimeSignatureSettings, feature_category: :not_owned do
+RSpec.describe SmimeSignatureSettings, feature_category: :shared do
describe '.parse' do
let(:default_smime_key) { Rails.root.join('.gitlab_smime_key') }
let(:default_smime_cert) { Rails.root.join('.gitlab_smime_cert') }
diff --git a/spec/contracts/provider/helpers/contract_source_helper.rb b/spec/contracts/provider/helpers/contract_source_helper.rb
index f59f228722d..e1891b316f3 100644
--- a/spec/contracts/provider/helpers/contract_source_helper.rb
+++ b/spec/contracts/provider/helpers/contract_source_helper.rb
@@ -2,7 +2,6 @@
module Provider
module ContractSourceHelper
- QA_PACT_BROKER_HOST = "http://localhost:9292/pacts"
PREFIX_PATHS = {
rake: {
ce: "../../contracts/project",
@@ -10,7 +9,7 @@ module Provider
},
spec: "../contracts/project"
}.freeze
- SUB_PATH_REGEX = %r{project/(?<file_path>.*?)_helper.rb}.freeze
+ SUB_PATH_REGEX = %r{project/(?<file_path>.*?)_helper.rb}
class << self
def contract_location(requester:, file_path:, edition: :ce)
@@ -26,7 +25,7 @@ module Provider
provider_url = "provider/#{construct_provider_url_path(file_path)}"
consumer_url = "consumer/#{construct_consumer_url_path(file_path)}"
- "#{QA_PACT_BROKER_HOST}/#{provider_url}/#{consumer_url}/latest"
+ "#{ENV['QA_PACT_BROKER_HOST']}/pacts/#{provider_url}/#{consumer_url}/latest"
end
def construct_provider_url_path(file_path)
diff --git a/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb b/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb
index 39537aa153d..18da71e0601 100644
--- a/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb
+++ b/spec/contracts/provider_specs/helpers/provider/contract_source_helper_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_relative '../../../provider/helpers/contract_source_helper'
-RSpec.describe Provider::ContractSourceHelper, feature_category: :not_owned do
+RSpec.describe Provider::ContractSourceHelper, feature_category: :shared do
let(:pact_helper_path) { 'pact_helpers/project/pipelines/new/post_create_pipeline_helper.rb' }
let(:split_pact_helper_path) { %w[pipelines new post_create_pipeline] }
let(:provider_url_path) { 'POST%20create%20pipeline' }
@@ -54,8 +54,12 @@ RSpec.describe Provider::ContractSourceHelper, feature_category: :not_owned do
end
describe '#pact_broker_url' do
+ before do
+ stub_env('QA_PACT_BROKER_HOST', 'http://localhost')
+ end
+
it 'returns the full url to the contract that the provider test is verifying' do
- contract_url_path = "http://localhost:9292/pacts/provider/" \
+ contract_url_path = "http://localhost/pacts/provider/" \
"#{provider_url_path}/consumer/#{consumer_url_path}/latest"
expect(subject.pact_broker_url(split_pact_helper_path)).to eq(contract_url_path)
diff --git a/spec/contracts/publish-contracts.sh b/spec/contracts/publish-contracts.sh
index 8b9d4b6ecc6..b50ba9afae8 100644
--- a/spec/contracts/publish-contracts.sh
+++ b/spec/contracts/publish-contracts.sh
@@ -1,6 +1,5 @@
LATEST_SHA=$(git rev-parse HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
-BROKER_BASE_URL="http://localhost:9292"
cd "${0%/*}" || exit 1
@@ -18,7 +17,7 @@ function publish_contract () {
for contract in $CONTRACTS
do
printf "\e[32mPublishing %s...\033[0m\n" "$contract"
- pact-broker publish "$contract" --consumer-app-version "$LATEST_SHA" --branch "$GIT_BRANCH" --broker-base-url "$BROKER_BASE_URL" --output json
+ pact-broker publish "$contract" --consumer-app-version "$LATEST_SHA" --branch "$GIT_BRANCH" --broker-base-url "$QA_PACT_BROKER_HOST" --output json
done
if [ ${ERROR} = 1 ]; then
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 32ac0f8dc07..253f66579f3 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -400,7 +400,7 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
end
end
- describe 'PUT #reset_registration_token', feature_category: :credential_management do
+ describe 'PUT #reset_registration_token', feature_category: :user_management do
before do
sign_in(admin)
end
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
index bf7707f177c..edb17aefe86 100644
--- a/spec/controllers/admin/applications_controller_spec.rb
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -38,44 +38,43 @@ RSpec.describe Admin::ApplicationsController do
end
end
- describe 'POST #create' do
- 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: true, confidential: false, scopes: ['api'])
-
- expect do
- post :create, params: { doorkeeper_application: create_params }
- end.to change { Doorkeeper::Application.count }.by(1)
+ describe 'PUT #renew' do
+ let(:oauth_params) do
+ {
+ id: application.id
+ }
+ end
- application = Doorkeeper::Application.last
+ subject { put :renew, params: oauth_params }
- expect(response).to redirect_to(admin_application_path(application))
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
- end
- end
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ it { expect { subject }.to change { application.reload.secret } }
- context 'with hash_oauth_secrets flag on' do
+ context 'when renew fails' do
before do
- stub_feature_flags(hash_oauth_secrets: true)
+ allow_next_found_instance_of(Doorkeeper::Application) do |application|
+ allow(application).to receive(:save).and_return(false)
+ end
end
- it 'creates the application' do
- create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
+ it { expect { subject }.not_to change { application.reload.secret } }
+ it { is_expected.to redirect_to(admin_application_url(application)) }
+ end
+ end
- expect do
- post :create, params: { doorkeeper_application: create_params }
- end.to change { Doorkeeper::Application.count }.by(1)
+ describe 'POST #create' do
+ it 'creates the application' do
+ create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api'])
- application = Doorkeeper::Application.last
+ expect do
+ post :create, params: { doorkeeper_application: create_params }
+ end.to change { Doorkeeper::Application.count }.by(1)
- 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
+ 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
it 'renders the application form on errors' do
@@ -88,43 +87,18 @@ RSpec.describe Admin::ApplicationsController do
end
context 'when the params are for a confidential application' do
- context 'with hash_oauth_secrets flag off' do
- before do
- stub_feature_flags(hash_oauth_secrets: false)
- 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 redirect_to(admin_application_path(application))
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
- end
- end
+ it 'creates a confidential application' do
+ create_params = attributes_for(:application, confidential: true, scopes: ['read_user'])
- 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)
+ 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 have_gitlab_http_status(:ok)
- expect(response).to render_template :show
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
- end
+ 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
diff --git a/spec/controllers/admin/cohorts_controller_spec.rb b/spec/controllers/admin/cohorts_controller_spec.rb
index 50626a5da91..26f6540258e 100644
--- a/spec/controllers/admin/cohorts_controller_spec.rb
+++ b/spec/controllers/admin/cohorts_controller_spec.rb
@@ -17,7 +17,6 @@ RSpec.describe Admin::CohortsController do
it_behaves_like 'Snowplow event tracking with RedisHLL context' 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' }
diff --git a/spec/controllers/admin/dev_ops_report_controller_spec.rb b/spec/controllers/admin/dev_ops_report_controller_spec.rb
index 52a46b5e99a..d8166380760 100644
--- a/spec/controllers/admin/dev_ops_report_controller_spec.rb
+++ b/spec/controllers/admin/dev_ops_report_controller_spec.rb
@@ -32,7 +32,6 @@ RSpec.describe Admin::DevOpsReportController do
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject { get :show, format: :html }
- 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' }
diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb
index e75f27589d7..fed96f6a6c7 100644
--- a/spec/controllers/admin/integrations_controller_spec.rb
+++ b/spec/controllers/admin/integrations_controller_spec.rb
@@ -29,11 +29,7 @@ RSpec.describe Admin::IntegrationsController do
end
end
- context 'when GitLab.com' do
- before do
- allow(::Gitlab).to receive(:com?) { true }
- end
-
+ context 'when GitLab.com', :saas do
it 'returns 404' do
get :edit, params: { id: Integration.available_integration_names.sample }
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index a39a1f38a11..b1a2d90589a 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -35,26 +35,59 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
end
describe '#new' do
- context 'when create_runner_workflow is enabled' do
+ it 'renders a :new template' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:new)
+ end
+
+ context 'when create_runner_workflow_for_admin is disabled' do
before do
- stub_feature_flags(create_runner_workflow: true)
+ stub_feature_flags(create_runner_workflow_for_admin: false)
end
- it 'renders a :new template' do
+ it 'returns :not_found' do
get :new
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe '#register' do
+ subject(:register) { get :register, params: { id: new_runner.id } }
+
+ context 'when runner can be registered after creation' do
+ let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
+
+ it 'renders a :register template' do
+ register
+
expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:new)
+ expect(response).to render_template(:register)
end
end
- context 'when create_runner_workflow is disabled' do
+ context 'when runner cannot be registered after creation' do
+ let_it_be(:new_runner) { runner }
+
+ it 'returns :not_found' do
+ register
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when create_runner_workflow_for_admin is disabled' do
+ let_it_be(:new_runner) { create(:ci_runner, registration_type: :authenticated_user) }
+
before do
- stub_feature_flags(create_runner_workflow: false)
+ stub_feature_flags(create_runner_workflow_for_admin: false)
end
it 'returns :not_found' do
- get :new
+ register
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb
index 5fa7a7f278d..07088eed6d4 100644
--- a/spec/controllers/admin/sessions_controller_spec.rb
+++ b/spec/controllers/admin/sessions_controller_spec.rb
@@ -220,7 +220,9 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
end
end
- shared_examples 'when using two-factor authentication via hardware device' do
+ context 'when using two-factor authentication via WebAuthn' do
+ let(:user) { create(:admin, :two_factor_via_webauthn) }
+
def authenticate_2fa(user_params)
post(:create, params: { user: user_params }, session: { otp_user_id: user.id })
end
@@ -237,10 +239,6 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
end
it 'can login with valid auth' do
- # we can stub both without an differentiation between webauthn / u2f
- # as these not interfere with each other und this saves us passing aroud
- # parameters
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
expect(controller.current_user_mode.admin_mode?).to be(false)
@@ -255,7 +253,6 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
end
it 'cannot login with invalid auth' do
- allow(U2fRegistration).to receive(:authenticate).and_return(false)
allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
@@ -267,22 +264,6 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
-
- context 'when using two-factor authentication via U2F' do
- it_behaves_like 'when using two-factor authentication via hardware device' do
- let(:user) { create(:admin, :two_factor_via_u2f) }
-
- before do
- stub_feature_flags(webauthn: false)
- end
- end
- end
-
- context 'when using two-factor authentication via WebAuthn' do
- it_behaves_like 'when using two-factor authentication via hardware device' do
- let(:user) { create(:admin, :two_factor_via_webauthn) }
- end
- end
end
end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 51f7ecdece6..b39c3bd009b 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::SpamLogsController do
+RSpec.describe Admin::SpamLogsController, feature_category: :instance_resiliency do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let!(:first_spam) { create(:spam_log, user: user) }
@@ -13,9 +13,10 @@ RSpec.describe Admin::SpamLogsController do
end
describe '#index' do
- it 'lists all spam logs' do
+ it 'lists paginated spam logs' do
get :index
+ expect(assigns(:spam_logs)).to be_kind_of(Kaminari::PaginatableWithoutCount)
expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -33,10 +34,7 @@ RSpec.describe Admin::SpamLogsController do
end.not_to change { SpamLog.count }
expect(response).to have_gitlab_http_status(:found)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- ).to be_exists
+ 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
diff --git a/spec/controllers/admin/usage_trends_controller_spec.rb b/spec/controllers/admin/usage_trends_controller_spec.rb
index 87cf8988b4e..7b801d53f14 100644
--- a/spec/controllers/admin/usage_trends_controller_spec.rb
+++ b/spec/controllers/admin/usage_trends_controller_spec.rb
@@ -17,7 +17,6 @@ RSpec.describe Admin::UsageTrendsController do
it_behaves_like 'Snowplow event tracking with RedisHLL context' 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' }
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 63e68118066..ec2559550c3 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -185,22 +185,14 @@ RSpec.describe Admin::UsersController do
delete :destroy, params: { id: user.username }, format: :json
expect(response).to have_gitlab_http_status(:ok)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: false)
- ).to be_exists
+ expect(Users::GhostUserMigration.where(user: user, initiator_user: admin, hard_delete: false)).to be_exists
end
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(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: true)
- ).to be_exists
+ expect(Users::GhostUserMigration.where(user: user, initiator_user: admin, hard_delete: true)).to be_exists
end
context 'prerequisites for account deletion' do
@@ -231,11 +223,7 @@ RSpec.describe Admin::UsersController do
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
+ expect(Users::GhostUserMigration.where(user: user, initiator_user: admin, hard_delete: true)).to be_exists
end
end
end
@@ -252,10 +240,7 @@ RSpec.describe Admin::UsersController do
it 'initiates user removal', :sidekiq_inline do
subject
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- ).to be_exists
+ expect(Users::GhostUserMigration.where(user: user, initiator_user: admin)).to be_exists
end
it 'displays the rejection message' do
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index f1adb9020fa..35e374d3b7f 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ApplicationController do
+RSpec.describe ApplicationController, feature_category: :shared do
include TermsHelper
let(:user) { create(:user) }
@@ -736,23 +736,11 @@ RSpec.describe ApplicationController do
end
end
- context 'user not logged in' do
- it 'sets the default headers' do
- get :index
-
- expect(response.headers['Cache-Control']).to be_nil
- expect(response.headers['Pragma']).to be_nil
- end
- end
-
- context 'user logged in' do
- it 'sets the default headers' do
- sign_in(user)
-
- get :index
+ it 'sets the default headers' do
+ get :index
- expect(response.headers['Pragma']).to eq 'no-cache'
- end
+ expect(response.headers['Cache-Control']).to be_nil
+ expect(response.headers['Pragma']).to be_nil
end
end
@@ -779,7 +767,6 @@ RSpec.describe ApplicationController do
subject
expect(response.headers['Cache-Control']).to eq 'private, no-store'
- expect(response.headers['Pragma']).to eq 'no-cache'
expect(response.headers['Expires']).to eq 'Fri, 01 Jan 1990 00:00:00 GMT'
end
diff --git a/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
index 246119a8118..a6a0f2c11b4 100644
--- a/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
+++ b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::ValueStreamActions, type: :controller,
-feature_category: :planning_analytics do
+ feature_category: :planning_analytics do
subject(:controller) do
Class.new(ApplicationController) do
include Analytics::CycleAnalytics::ValueStreamActions
diff --git a/spec/controllers/concerns/confirm_email_warning_spec.rb b/spec/controllers/concerns/confirm_email_warning_spec.rb
index 334c156e1ae..fca99d37000 100644
--- a/spec/controllers/concerns/confirm_email_warning_spec.rb
+++ b/spec/controllers/concerns/confirm_email_warning_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ConfirmEmailWarning do
before do
- stub_feature_flags(soft_email_confirmation: true)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
end
controller(ApplicationController) do
diff --git a/spec/controllers/concerns/content_security_policy_patch_spec.rb b/spec/controllers/concerns/content_security_policy_patch_spec.rb
index 6322950977c..9b4ddb35993 100644
--- a/spec/controllers/concerns/content_security_policy_patch_spec.rb
+++ b/spec/controllers/concerns/content_security_policy_patch_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
# Based on https://github.com/rails/rails/pull/45115/files#diff-35ef6d1bd8b8d3b037ec819a704cd78db55db916a57abfc2859882826fc679b6
-RSpec.describe ContentSecurityPolicyPatch, feature_category: :not_owned do
+RSpec.describe ContentSecurityPolicyPatch, feature_category: :shared do
include Rack::Test::Methods
let(:routes) do
diff --git a/spec/controllers/concerns/continue_params_spec.rb b/spec/controllers/concerns/continue_params_spec.rb
index ba600b8156a..9ac7087430e 100644
--- a/spec/controllers/concerns/continue_params_spec.rb
+++ b/spec/controllers/concerns/continue_params_spec.rb
@@ -31,10 +31,7 @@ RSpec.describe ContinueParams do
it 'cleans up any params that are not allowed' do
allow(controller).to receive(:params) do
- strong_continue_params(to: '/hello',
- notice: 'world',
- notice_now: '!',
- something: 'else')
+ strong_continue_params(to: '/hello', notice: 'world', notice_now: '!', something: 'else')
end
expect(controller.continue_params.keys).to contain_exactly(*%w(to notice notice_now))
diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
index a58b83dc42c..fc8b1efd226 100644
--- a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
+++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
@@ -25,9 +25,7 @@ RSpec.describe ControllerWithCrossProjectAccessCheck do
# `described_class` is not available in this context
include ControllerWithCrossProjectAccessCheck
- requires_cross_project_access :index, show: false,
- unless: -> { unless_condition },
- if: -> { if_condition }
+ requires_cross_project_access :index, show: false, unless: -> { unless_condition }, if: -> { if_condition }
def index
head :ok
@@ -86,9 +84,10 @@ RSpec.describe ControllerWithCrossProjectAccessCheck do
requires_cross_project_access
- skip_cross_project_access_check index: true, show: false,
- unless: -> { unless_condition },
- if: -> { if_condition }
+ skip_cross_project_access_check index: true,
+ show: false,
+ unless: -> { unless_condition },
+ if: -> { if_condition }
def index
head :ok
diff --git a/spec/controllers/concerns/kas_cookie_spec.rb b/spec/controllers/concerns/kas_cookie_spec.rb
new file mode 100644
index 00000000000..e2ca19457ff
--- /dev/null
+++ b/spec/controllers/concerns/kas_cookie_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe KasCookie, feature_category: :kubernetes_management do
+ describe '#set_kas_cookie' do
+ controller(ApplicationController) do
+ include KasCookie
+
+ def index
+ set_kas_cookie
+
+ render json: {}, status: :ok
+ end
+ end
+
+ before do
+ allow(::Gitlab::Kas).to receive(:enabled?).and_return(true)
+ end
+
+ subject(:kas_cookie) do
+ get :index
+
+ request.env['action_dispatch.cookies'][Gitlab::Kas::COOKIE_KEY]
+ end
+
+ context 'when user is signed out' do
+ it { is_expected.to be_blank }
+ end
+
+ context 'when user is signed in' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'sets the KAS cookie', :aggregate_failures do
+ allow(::Gitlab::Kas::UserAccess).to receive(:cookie_data).and_return('foobar')
+
+ expect(kas_cookie).to be_present
+ expect(kas_cookie).to eq('foobar')
+ expect(::Gitlab::Kas::UserAccess).to have_received(:cookie_data)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(kas_user_access: false)
+ end
+
+ it { is_expected.to be_blank }
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb
index 12b4065b89c..b0074b52aa2 100644
--- a/spec/controllers/concerns/product_analytics_tracking_spec.rb
+++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb
@@ -8,7 +8,11 @@ RSpec.describe ProductAnalyticsTracking, :snowplow, feature_category: :product_a
let(:user) { create(:user) }
let(:event_name) { 'an_event' }
+ let(:event_action) { 'an_action' }
+ let(:event_label) { 'a_label' }
+
let!(:group) { create(:group) }
+ let_it_be(:project) { create(:project) }
before do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
@@ -19,8 +23,15 @@ RSpec.describe ProductAnalyticsTracking, :snowplow, feature_category: :product_a
include ProductAnalyticsTracking
skip_before_action :authenticate_user!, only: :show
- track_event(:index, :show, name: 'an_event', destinations: [:redis_hll, :snowplow],
- conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id }
+ track_event(
+ :index,
+ :show,
+ name: 'an_event',
+ action: 'an_action',
+ label: 'a_label',
+ destinations: [:redis_hll, :snowplow],
+ conditions: [:custom_condition_one?, :custom_condition_two?]
+ ) { |controller| controller.get_custom_id }
def index
render html: 'index'
@@ -44,6 +55,10 @@ RSpec.describe ProductAnalyticsTracking, :snowplow, feature_category: :product_a
Group.first
end
+ def tracking_project_source
+ Project.first
+ end
+
def custom_condition_one?
true
end
@@ -64,7 +79,10 @@ RSpec.describe ProductAnalyticsTracking, :snowplow, feature_category: :product_a
expect_snowplow_event(
category: anything,
- action: event_name,
+ action: event_action,
+ property: event_name,
+ label: event_label,
+ project: project,
namespace: group,
user: user,
context: [context]
diff --git a/spec/controllers/concerns/renders_commits_spec.rb b/spec/controllers/concerns/renders_commits_spec.rb
index 6a504681527..45f194b63e7 100644
--- a/spec/controllers/concerns/renders_commits_spec.rb
+++ b/spec/controllers/concerns/renders_commits_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe RendersCommits do
@merge_request = MergeRequest.find(params[:id])
@commits = set_commits_for_rendering(
@merge_request.recent_commits.with_latest_pipeline(@merge_request.source_branch),
- commits_count: @merge_request.commits_count
+ commits_count: @merge_request.commits_count
)
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 6acbff6e745..3fe6d62329e 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe SendFileUpload do
Class.new(GitlabUploader) do
include ObjectStorage::Concern
- storage_options Gitlab.config.uploads
+ storage_location :uploads
private
diff --git a/spec/controllers/concerns/sorting_preference_spec.rb b/spec/controllers/concerns/sorting_preference_spec.rb
index 82a920215ca..6880d83142d 100644
--- a/spec/controllers/concerns/sorting_preference_spec.rb
+++ b/spec/controllers/concerns/sorting_preference_spec.rb
@@ -26,11 +26,14 @@ RSpec.describe SortingPreference do
describe '#set_sort_order' do
let(:group) { build(:group) }
+ let(:controller_name) { 'issues' }
+ let(:action_name) { 'issues' }
let(:issue_weights_available) { true }
before do
allow(controller).to receive(:default_sort_order).and_return('updated_desc')
- allow(controller).to receive(:action_name).and_return('issues')
+ allow(controller).to receive(:controller_name).and_return(controller_name)
+ allow(controller).to receive(:action_name).and_return(action_name)
allow(controller).to receive(:can_sort_by_issue_weight?).and_return(issue_weights_available)
user.user_preference.update!(issues_sort: sorting_field)
end
@@ -62,6 +65,42 @@ RSpec.describe SortingPreference do
end
end
end
+
+ context 'when user preference contains merged date sorting' do
+ let(:sorting_field) { 'merged_at_desc' }
+ let(:can_sort_by_merged_date?) { false }
+
+ before do
+ allow(controller)
+ .to receive(:can_sort_by_merged_date?)
+ .with(can_sort_by_merged_date?)
+ .and_return(can_sort_by_merged_date?)
+ end
+
+ it 'sets default sort order' do
+ is_expected.to eq('updated_desc')
+ end
+
+ shared_examples 'user can sort by merged date' do
+ it 'sets sort order from user_preference' do
+ is_expected.to eq('merged_at_desc')
+ end
+ end
+
+ context 'when controller_name is merge_requests' do
+ let(:controller_name) { 'merge_requests' }
+ let(:can_sort_by_merged_date?) { true }
+
+ it_behaves_like 'user can sort by merged date'
+ end
+
+ context 'when action_name is merge_requests' do
+ let(:action_name) { 'merge_requests' }
+ let(:can_sort_by_merged_date?) { true }
+
+ it_behaves_like 'user can sort by merged date'
+ end
+ end
end
describe '#set_sort_order_from_user_preference' do
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
index 773a416dcb4..b1aa40d4d7b 100644
--- a/spec/controllers/confirmations_controller_spec.rb
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ConfirmationsController do
+RSpec.describe ConfirmationsController, feature_category: :system_access do
include DeviseHelpers
before do
@@ -58,8 +58,7 @@ RSpec.describe ConfirmationsController do
m.call(*args)
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'ConfirmationsController#show')
+ .to include('meta.user' => user.username, 'meta.caller_id' => 'ConfirmationsController#show')
end
perform_request
@@ -94,8 +93,7 @@ RSpec.describe ConfirmationsController do
m.call(*args)
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'ConfirmationsController#show')
+ .to include('meta.user' => user.username, 'meta.caller_id' => 'ConfirmationsController#show')
end
travel_to(3.days.from_now) { perform_request }
@@ -150,51 +148,71 @@ RSpec.describe ConfirmationsController do
end
end
- context 'when reCAPTCHA is disabled' do
+ context "when `email_confirmation_setting` is set to `soft`" do
before do
- stub_application_setting(recaptcha_enabled: false)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
end
- it 'successfully sends password reset when reCAPTCHA is not solved' do
- perform_request
+ context 'when reCAPTCHA is disabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
- expect(response).to redirect_to(dashboard_projects_path)
- end
- end
+ it 'successfully sends password reset when reCAPTCHA is not solved' do
+ perform_request
- context 'when reCAPTCHA is enabled' do
- before do
- stub_application_setting(recaptcha_enabled: true)
+ expect(response).to redirect_to(dashboard_projects_path)
+ end
end
- context 'when the reCAPTCHA is not solved' do
+ context 'when reCAPTCHA is enabled' do
before do
- Recaptcha.configuration.skip_verify_env.delete('test')
+ stub_application_setting(recaptcha_enabled: true)
end
- it 'displays an error' do
- perform_request
+ context 'when the reCAPTCHA is not solved' do
+ before do
+ Recaptcha.configuration.skip_verify_env.delete('test')
+ end
- expect(response).to render_template(:new)
- expect(flash[:alert]).to include _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
+ it 'displays an error' do
+ alert_text = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
+
+ perform_request
+
+ expect(response).to render_template(:new)
+ expect(flash[:alert]).to include alert_text
+ end
+
+ it 'sets gon variables' do
+ Gon.clear
+
+ perform_request
+
+ expect(response).to render_template(:new)
+ expect(Gon.all_variables).not_to be_empty
+ end
end
- it 'sets gon variables' do
- Gon.clear
+ it 'successfully sends password reset when reCAPTCHA is solved' do
+ Recaptcha.configuration.skip_verify_env << 'test'
perform_request
- expect(response).to render_template(:new)
- expect(Gon.all_variables).not_to be_empty
+ expect(response).to redirect_to(dashboard_projects_path)
end
end
+ end
- it 'successfully sends password reset when reCAPTCHA is solved' do
- Recaptcha.configuration.skip_verify_env << 'test'
+ context "when `email_confirmation_setting` is not set to `soft`" do
+ before do
+ stub_feature_flags(soft_email_confirmation: false)
+ end
+ it 'redirects to the users_almost_there path' do
perform_request
- expect(response).to redirect_to(dashboard_projects_path)
+ expect(response).to redirect_to(users_almost_there_path)
end
end
end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index e8ee146a13a..0e4771b20f7 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures, feature_categ
before_all do
project.add_developer(user)
project2.add_developer(user)
+ user.toggle_star(project2)
end
before do
@@ -39,6 +40,21 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures, feature_categ
expect(assigns(:projects)).to eq(projects)
end
+ it 'assigns the correct total_user_projects_count' do
+ get :index
+ total_user_projects_count = assigns(:total_user_projects_count)
+
+ expect(total_user_projects_count.count).to eq(2)
+ end
+
+ it 'assigns the correct total_starred_projects_count' do
+ get :index
+ total_starred_projects_count = assigns(:total_starred_projects_count)
+
+ expect(total_starred_projects_count.count).to eq(1)
+ expect(total_starred_projects_count).to include(project2)
+ end
+
context 'project sorting' do
it_behaves_like 'set sort order from user preference' do
let(:sorting_param) { 'created_asc' }
@@ -62,6 +78,36 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures, feature_categ
end
end
+ context 'with archived project' do
+ let_it_be(:archived_project) do
+ project2.tap { |p| p.update!(archived: true) }
+ end
+
+ it 'does not display archived project' do
+ get :index
+ projects_result = assigns(:projects)
+
+ expect(projects_result).not_to include(archived_project)
+ expect(projects_result).to include(project)
+ end
+
+ it 'excludes archived project from total_user_projects_count' do
+ get :index
+ total_user_projects_count = assigns(:total_user_projects_count)
+
+ expect(total_user_projects_count.count).to eq(1)
+ expect(total_user_projects_count).not_to include(archived_project)
+ end
+
+ it 'excludes archived project from total_starred_projects_count' do
+ get :index
+ total_starred_projects_count = assigns(:total_starred_projects_count)
+
+ expect(total_starred_projects_count.count).to eq(0)
+ expect(total_starred_projects_count).not_to include(archived_project)
+ end
+ end
+
context 'with deleted project' do
let!(:pending_delete_project) do
project.tap { |p| p.update!(pending_delete: true) }
diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb
index 902872b6e92..b76da85ad72 100644
--- a/spec/controllers/every_controller_spec.rb
+++ b/spec/controllers/every_controller_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
+
RSpec.describe "Every controller" do
context "feature categories" do
let_it_be(:feature_categories) do
@@ -52,7 +53,7 @@ RSpec.describe "Every controller" do
non_existing_used_actions = used_actions - existing_actions
expect(non_existing_used_actions).to be_empty,
- "#{controller} used #{non_existing_used_actions} to define feature category, but the route does not exist"
+ "#{controller} used #{non_existing_used_actions} to define feature category, but the route does not exist"
end
end
end
diff --git a/spec/controllers/explore/groups_controller_spec.rb b/spec/controllers/explore/groups_controller_spec.rb
index a3bd8102462..76bd94fd681 100644
--- a/spec/controllers/explore/groups_controller_spec.rb
+++ b/spec/controllers/explore/groups_controller_spec.rb
@@ -41,9 +41,9 @@ RSpec.describe Explore::GroupsController do
it_behaves_like 'explore groups'
- context 'generic_explore_groups flag is disabled' do
+ context 'gitlab.com' do
before do
- stub_feature_flags(generic_explore_groups: false)
+ allow(Gitlab).to receive(:com?).and_return(true)
end
it_behaves_like 'explore groups'
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 7aad67b01e8..cd72dbe394a 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -43,8 +43,9 @@ RSpec.describe GraphqlController, feature_category: :integrations do
post :execute
expect(json_response).to include(
- 'errors' => include(a_hash_including('message' => /Internal server error/,
- 'raisedAt' => /graphql_controller_spec.rb/))
+ 'errors' => include(
+ a_hash_including('message' => /Internal server error/, 'raisedAt' => /graphql_controller_spec.rb/)
+ )
)
end
@@ -108,6 +109,41 @@ RSpec.describe GraphqlController, feature_category: :integrations do
])
end
+ it 'executes a multiplexed queries with variables with no errors' do
+ query = <<~GQL
+ mutation($a: String!, $b: String!) {
+ echoCreate(input: { messages: [$a, $b] }) { echoes }
+ }
+ GQL
+ multiplex = [
+ { query: query, variables: { a: 'A', b: 'B' } },
+ { query: query, variables: { a: 'a', b: 'b' } }
+ ]
+
+ post :execute, params: { _json: multiplex }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(
+ [
+ { 'data' => { 'echoCreate' => { 'echoes' => %w[A B] } } },
+ { 'data' => { 'echoCreate' => { 'echoes' => %w[a b] } } }
+ ])
+ end
+
+ it 'does not allow string as _json parameter' do
+ post :execute, params: { _json: 'bad' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ "errors" => [
+ {
+ "message" => "Unexpected end of document",
+ "locations" => []
+ }
+ ]
+ })
+ end
+
it 'sets a limit on the total query size' do
graphql_query = "{#{(['__typename'] * 1000).join(' ')}}"
@@ -172,14 +208,28 @@ RSpec.describe GraphqlController, feature_category: :integrations do
post :execute
end
- it 'calls the track gitlab cli when trackable method' do
- agent = 'GLab - GitLab CLI'
- request.env['HTTP_USER_AGENT'] = agent
+ context 'if using the GitLab CLI' do
+ it 'call trackable for the old UserAgent' do
+ agent = 'GLab - GitLab CLI'
- expect(Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter)
- .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+ request.env['HTTP_USER_AGENT'] = agent
- post :execute
+ expect(Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ post :execute
+ end
+
+ it 'call trackable for the current UserAgent' do
+ agent = 'glab/v1.25.3-27-g7ec258fb (built 2023-02-16), darwin'
+
+ request.env['HTTP_USER_AGENT'] = agent
+
+ expect(Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter)
+ .to receive(:track_api_request_when_trackable).with(user_agent: agent, user: user)
+
+ post :execute
+ end
end
it "assigns username in ApplicationContext" do
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index d0656ee47ce..2e37ed95c1c 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -275,6 +275,18 @@ RSpec.describe Groups::ChildrenController, feature_category: :subgroups do
allow(Kaminari.config).to receive(:default_per_page).and_return(per_page)
end
+ it 'rejects negative per_page parameter' do
+ get :index, params: { group_id: group.to_param, per_page: -1 }, format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'rejects non-numeric per_page parameter' do
+ get :index, params: { group_id: group.to_param, per_page: 'abc' }, format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
context 'with only projects' do
let!(:other_project) { create(:project, :public, namespace: group) }
let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) }
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index f1ca9e11a1a..a59c90a3cf2 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -249,7 +249,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
expect(send_data_type).to eq('send-dependency')
expect(header).to eq(
"Authorization" => ["Bearer abcd1234"],
- "Accept" => ::ContainerRegistry::Client::ACCEPTED_TYPES
+ "Accept" => ::DependencyProxy::Manifest::ACCEPTED_TYPES
)
expect(url).to eq(DependencyProxy::Registry.manifest_url(image, tag))
expect(response.headers['Content-Type']).to eq('application/gzip')
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 4e5dc01f466..35efcb664c0 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -55,6 +55,20 @@ RSpec.describe Groups::GroupMembersController do
expect(assigns(:invited_members).count).to eq(1)
end
+
+ context 'when filtering by user type' do
+ let_it_be(:service_account) { create(:user, :service_account) }
+
+ before do
+ group.add_developer(service_account)
+ end
+
+ it 'returns only service accounts' do
+ get :index, params: { group_id: group, user_type: 'service_account' }
+
+ expect(assigns(:members).map(&:user_id)).to match_array([service_account.id])
+ end
+ end
end
context 'when user cannot manage members' do
@@ -67,6 +81,21 @@ RSpec.describe Groups::GroupMembersController do
expect(assigns(:invited_members)).to be_nil
end
+
+ context 'when filtering by user type' do
+ let_it_be(:service_account) { create(:user, :service_account) }
+
+ before do
+ group.add_developer(user)
+ group.add_developer(service_account)
+ end
+
+ it 'returns only service accounts' do
+ get :index, params: { group_id: group, user_type: 'service_account' }
+
+ expect(assigns(:members).map(&:user_id)).to match_array([user.id, service_account.id])
+ end
+ end
end
context 'when user has owner access to subgroup' do
@@ -489,13 +518,11 @@ RSpec.describe Groups::GroupMembersController do
describe 'PUT #update' do
it 'is successful' do
- put :update,
- params: {
- group_member: { access_level: Gitlab::Access::GUEST },
- group_id: group,
- id: membership
- },
- format: :json
+ put :update, params: {
+ group_member: { access_level: Gitlab::Access::GUEST },
+ group_id: group,
+ id: membership
+ }, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index a3c4c47ab15..f4046cb97a0 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -230,11 +230,10 @@ RSpec.describe Groups::MilestonesController do
describe "#create" do
it "creates group milestone with Chinese title" do
- post :create,
- params: {
- group_id: group.to_param,
- milestone: milestone_params
- }
+ post :create, params: {
+ group_id: group.to_param,
+ milestone: milestone_params
+ }
milestone = Milestone.find_by_title(title)
@@ -251,12 +250,11 @@ RSpec.describe Groups::MilestonesController do
it "updates group milestone" do
milestone_params[:title] = "title changed"
- put :update,
- params: {
- id: milestone.iid,
- group_id: group.to_param,
- milestone: milestone_params
- }
+ put :update, params: {
+ id: milestone.iid,
+ group_id: group.to_param,
+ milestone: milestone_params
+ }
milestone.reload
expect(response).to redirect_to(group_milestone_path(group, milestone.iid))
@@ -390,21 +388,19 @@ RSpec.describe Groups::MilestonesController do
context 'for a non-GET request' do
context 'when requesting the canonical path with different casing' do
it 'does not 404' do
- post :create,
- params: {
- group_id: group.to_param,
- milestone: { title: title }
- }
+ post :create, params: {
+ group_id: group.to_param,
+ milestone: { title: title }
+ }
expect(response).not_to have_gitlab_http_status(:not_found)
end
it 'does not redirect to the correct casing' do
- post :create,
- params: {
- group_id: group.to_param,
- milestone: { title: title }
- }
+ post :create, params: {
+ group_id: group.to_param,
+ milestone: { title: title }
+ }
expect(response).not_to have_gitlab_http_status(:moved_permanently)
end
@@ -414,11 +410,10 @@ RSpec.describe Groups::MilestonesController do
let(:redirect_route) { group.redirect_routes.create!(path: 'old-path') }
it 'returns not found' do
- post :create,
- params: {
- group_id: redirect_route.path,
- milestone: { title: title }
- }
+ post :create, params: {
+ group_id: redirect_route.path,
+ milestone: { title: title }
+ }
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/controllers/groups/settings/applications_controller_spec.rb b/spec/controllers/groups/settings/applications_controller_spec.rb
index b9457770ed6..2fadac2dc17 100644
--- a/spec/controllers/groups/settings/applications_controller_spec.rb
+++ b/spec/controllers/groups/settings/applications_controller_spec.rb
@@ -71,43 +71,18 @@ RSpec.describe Groups::Settings::ApplicationsController do
group.add_owner(user)
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: 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 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'])
+ 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)
+ 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))
- end
+ 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
it 'renders the application form on errors' do
@@ -120,43 +95,18 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
context 'when the params are for a confidential application' do
- context 'with hash_oauth_secrets flag off' do
- before do
- stub_feature_flags(hash_oauth_secrets: false)
- 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 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'])
+ 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)
+ 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 have_gitlab_http_status(:ok)
- expect(response).to render_template :show
- expect(application).to have_attributes(create_params.except(:uid, :owner_type))
- end
+ 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
@@ -188,6 +138,55 @@ RSpec.describe Groups::Settings::ApplicationsController do
end
end
+ describe 'PUT #renew' do
+ context 'when user is owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ let(:oauth_params) do
+ {
+ group_id: group,
+ id: application.id
+ }
+ end
+
+ subject { put :renew, params: oauth_params }
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ it { expect { subject }.to change { application.reload.secret } }
+
+ context 'when renew fails' do
+ before do
+ allow_next_found_instance_of(Doorkeeper::Application) do |application|
+ allow(application).to receive(:save).and_return(false)
+ end
+ end
+
+ it { expect { subject }.not_to change { application.reload.secret } }
+ it { is_expected.to redirect_to(group_settings_application_url(group, application)) }
+ end
+ end
+
+ context 'when user is not owner' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ let(:oauth_params) do
+ {
+ group_id: group,
+ id: application.id
+ }
+ end
+
+ it 'renders a 404' do
+ put :renew, params: oauth_params
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'PATCH #update' do
context 'when user is owner' do
before do
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb
index 6dbe75bb1df..8c6efae89c3 100644
--- a/spec/controllers/groups/variables_controller_spec.rb
+++ b/spec/controllers/groups/variables_controller_spec.rb
@@ -77,12 +77,10 @@ RSpec.describe Groups::VariablesController do
describe 'PATCH #update' do
it 'is successful' do
- patch :update,
- params: {
- group_id: group,
- variables_attributes: [{ id: variable.id, key: 'hello' }]
- },
- format: :json
+ patch :update, params: {
+ group_id: group,
+ variables_attributes: [{ id: variable.id, key: 'hello' }]
+ }, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 2375146f346..ac6715bacd5 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -261,11 +261,7 @@ RSpec.describe HelpController do
context 'for image formats' do
context 'when requested file exists' do
it 'renders the raw file' do
- get :show,
- params: {
- path: 'user/img/markdown_logo'
- },
- format: :png
+ get :show, params: { path: 'user/img/markdown_logo' }, format: :png
aggregate_failures do
expect(response).to be_successful
@@ -277,11 +273,7 @@ RSpec.describe HelpController do
context 'when requested file is missing' do
it 'renders not found' do
- get :show,
- params: {
- path: 'foo/bar'
- },
- format: :png
+ get :show, params: { path: 'foo/bar' }, format: :png
expect(response).to be_not_found
end
end
@@ -289,11 +281,7 @@ RSpec.describe HelpController do
context 'for other formats' do
it 'always renders not found' do
- get :show,
- params: {
- path: 'user/ssh'
- },
- format: :foo
+ get :show, params: { path: 'user/ssh' }, format: :foo
expect(response).to be_not_found
end
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 35f712dc50d..055c98ebdbc 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -48,11 +48,13 @@ RSpec.describe Import::BitbucketController do
let(:expires_at) { Time.current + 1.day }
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)
+ double(
+ token: token,
+ secret: secret,
+ expires_at: expires_at,
+ expires_in: expires_in,
+ refresh_token: refresh_token
+ )
end
before do
@@ -63,10 +65,10 @@ RSpec.describe Import::BitbucketController do
allow_any_instance_of(OAuth2::Client)
.to receive(:get_token)
.with(hash_including(
- 'grant_type' => 'authorization_code',
- 'code' => code,
- 'redirect_uri' => users_import_bitbucket_callback_url),
- {})
+ 'grant_type' => 'authorization_code',
+ 'code' => code,
+ 'redirect_uri' => users_import_bitbucket_callback_url),
+ {})
.and_return(access_token)
stub_omniauth_provider('bitbucket')
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index a3992ae850e..4e7f9572f65 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -121,12 +121,12 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
params = { page: 1, per_page: 20, filter: '' }.merge(params_override)
get :status,
- params: params,
- format: format,
- session: {
- bulk_import_gitlab_url: 'https://gitlab.example.com',
- bulk_import_gitlab_access_token: 'demo-pat'
- }
+ params: params,
+ format: format,
+ session: {
+ bulk_import_gitlab_url: 'https://gitlab.example.com',
+ bulk_import_gitlab_access_token: 'demo-pat'
+ }
end
include_context 'bulk imports requests context', 'https://gitlab.example.com'
@@ -157,8 +157,7 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
end
let(:source_version) do
- Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION,
- ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT)
+ Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION, ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT)
end
before do
@@ -270,8 +269,7 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
context 'when connection error occurs' do
let(:source_version) do
- Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION,
- ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT)
+ Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION, ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT)
end
before do
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index ed2a588eadf..e2d59fc213a 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -116,8 +116,7 @@ RSpec.describe Import::FogbugzController do
describe 'GET status' do
let(:repo) do
- instance_double(Gitlab::FogbugzImport::Repository,
- id: 'demo', name: 'vim', safe_name: 'vim', path: 'vim')
+ instance_double(Gitlab::FogbugzImport::Repository, id: 'demo', name: 'vim', safe_name: 'vim', path: 'vim')
end
it 'redirects to new page form when client is invalid' do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 406a3604b23..4928cc46d7f 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe Import::GithubController, feature_category: :importers do
expect_next_instance_of(Gitlab::GithubImport::Clients::Proxy) do |client|
expect(client).to receive(:repos)
.with(expected_filter, expected_options)
- .and_return({ repos: [], page_info: {} })
+ .and_return({ repos: [], page_info: {}, count: 0 })
end
get :status, params: params, format: :json
@@ -149,6 +149,7 @@ RSpec.describe Import::GithubController, feature_category: :importers do
expect(json_response['provider_repos'].size).to eq 0
expect(json_response['incompatible_repos'].size).to eq 0
expect(json_response['page_info']).to eq({})
+ expect(json_response['provider_repo_count']).to eq(0)
end
end
@@ -161,9 +162,7 @@ RSpec.describe Import::GithubController, feature_category: :importers do
let(:provider_repos) { [] }
let(:expected_filter) { '' }
let(:expected_options) do
- pagination_params.merge(relation_params).merge(
- first: 25, page: 1, per_page: 25
- )
+ pagination_params.merge(relation_params).merge(first: 25)
end
before do
@@ -171,6 +170,9 @@ RSpec.describe Import::GithubController, feature_category: :importers do
if client_auth_success
allow(proxy).to receive(:repos).and_return({ repos: provider_repos })
allow(proxy).to receive(:client).and_return(client_stub)
+ allow_next_instance_of(Gitlab::GithubImport::ProjectRelationType) do |instance|
+ allow(instance).to receive(:for).with('example/repo').and_return('owned')
+ end
else
allow(proxy).to receive(:repos).and_raise(Octokit::Unauthorized)
end
@@ -279,22 +281,12 @@ RSpec.describe Import::GithubController, feature_category: :importers do
it_behaves_like 'calls repos through Clients::Proxy with expected args'
end
-
- context 'when page is specified' do
- let(:pagination_params) { { before: nil, after: nil, page: 2 } }
- let(:params) { pagination_params }
- let(:expected_options) do
- pagination_params.merge(relation_params).merge(first: 25, page: 2, per_page: 25)
- end
-
- it_behaves_like 'calls repos through Clients::Proxy with expected args'
- end
end
context 'when relation type params present' do
let(:organization_login) { 'test-login' }
let(:params) { pagination_params.merge(relation_type: 'organization', organization_login: organization_login) }
- let(:pagination_defaults) { { first: 25, page: 1, per_page: 25 } }
+ let(:pagination_defaults) { { first: 25 } }
let(:expected_options) do
pagination_defaults.merge(pagination_params).merge(
relation_type: 'organization', organization_login: organization_login
@@ -359,7 +351,13 @@ RSpec.describe Import::GithubController, feature_category: :importers do
end
end
- describe "POST create" do
+ describe "POST create", :clean_gitlab_redis_cache do
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::ProjectRelationType) do |instance|
+ allow(instance).to receive(:for).with("#{provider_username}/vim").and_return('owned')
+ end
+ end
+
it_behaves_like 'a GitHub-ish import controller: POST create'
it_behaves_like 'project import rate limiter'
@@ -388,13 +386,22 @@ RSpec.describe Import::GithubController, feature_category: :importers do
end
describe "POST cancel" do
- let_it_be(:project) { create(:project, :import_started, import_type: 'github', import_url: 'https://fake.url') }
+ let_it_be(:project) do
+ create(
+ :project, :import_started,
+ import_type: 'github', import_url: 'https://fake.url', import_source: 'login/repo'
+ )
+ end
context 'when project import was canceled' do
before do
allow(Import::Github::CancelProjectImportService)
.to receive(:new).with(project, user)
.and_return(double(execute: { status: :success, project: project }))
+
+ allow_next_instance_of(Gitlab::GithubImport::ProjectRelationType) do |instance|
+ allow(instance).to receive(:for).with('login/repo').and_return('owned')
+ end
end
it 'returns success' do
@@ -471,4 +478,26 @@ RSpec.describe Import::GithubController, feature_category: :importers do
end
end
end
+
+ describe 'GET counts' do
+ let(:expected_result) do
+ {
+ 'owned' => 3,
+ 'collaborated' => 2,
+ 'organization' => 1
+ }
+ end
+
+ it 'returns repos count by type' do
+ expect_next_instance_of(Gitlab::GithubImport::Clients::Proxy) do |client_proxy|
+ expect(client_proxy).to receive(:count_repos_by).with('owned', user.id).and_return(3)
+ expect(client_proxy).to receive(:count_repos_by).with('collaborated', user.id).and_return(2)
+ expect(client_proxy).to receive(:count_repos_by).with('organization', user.id).and_return(1)
+ end
+
+ get :counts
+
+ expect(json_response).to eq(expected_result)
+ end
+ end
end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index 9b16dc9a463..e7ec268a5a2 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -71,6 +71,33 @@ RSpec.describe Oauth::ApplicationsController do
it_behaves_like 'redirects to 2fa setup page when the user requires it'
end
+ describe 'PUT #renew' do
+ let(:oauth_params) do
+ {
+ id: application.id
+ }
+ end
+
+ subject { put :renew, params: oauth_params }
+
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ it { expect { subject }.to change { application.reload.secret } }
+
+ it_behaves_like 'redirects to login page when the user is not signed in'
+ it_behaves_like 'redirects to 2fa setup page when the user requires it'
+
+ context 'when renew fails' do
+ before do
+ allow_next_found_instance_of(Doorkeeper::Application) do |application|
+ allow(application).to receive(:save).and_return(false)
+ end
+ end
+
+ it { expect { subject }.not_to change { application.reload.secret } }
+ it { is_expected.to redirect_to(oauth_application_url(application)) }
+ end
+ end
+
describe 'GET #show' do
subject { get :show, params: { id: application.id } }
@@ -113,30 +140,11 @@ RSpec.describe Oauth::ApplicationsController do
subject { post :create, params: oauth_params }
- context 'when hash_oauth_tokens flag set' do
- before do
- stub_feature_flags(hash_oauth_secrets: true)
- end
-
- 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
+ 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
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
end
it 'redirects back to profile page if OAuth applications are disabled' do
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 5185aa64d9f..3476c7b8465 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Oauth::AuthorizationsController do
let(:application_scopes) { 'api read_user' }
let!(:application) do
- create(:oauth_application, scopes: application_scopes,
- redirect_uri: 'http://example.com')
+ create(:oauth_application, scopes: application_scopes, redirect_uri: 'http://example.com')
end
let(:params) do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index ab3f3fd397d..ebfa48870a9 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe OmniauthCallbacksController, type: :controller do
+RSpec.describe OmniauthCallbacksController, type: :controller, feature_category: :system_access do
include LoginHelpers
describe 'omniauth' do
@@ -202,20 +202,30 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
end
end
- context 'when user with 2FA is unconfirmed' do
+ context 'when a user has 2FA enabled' do
render_views
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider) }
- before do
- user.update_column(:confirmed_at, nil)
- end
+ context 'when a user is unconfirmed' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
- it 'redirects to login page' do
- post provider
+ user.update!(confirmed_at: nil)
+ end
+
+ it 'redirects to login page' do
+ post provider
+
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert]).to match(/You have to confirm your email address before continuing./)
+ end
+ end
- expect(response).to redirect_to(new_user_session_path)
- expect(flash[:alert]).to match(/You have to confirm your email address before continuing./)
+ context 'when a user is confirmed' do
+ it 'returns 200 response' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
@@ -324,9 +334,10 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args|
m.call(*args)
- expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2')
+ expect(Gitlab::ApplicationContext.current).to include(
+ 'meta.user' => user.username,
+ 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2'
+ )
end
post :atlassian_oauth2
@@ -419,6 +430,31 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
end
end
+ describe '#openid_connect' do
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
+ let(:extern_uid) { 'my-uid' }
+ let(:provider) { 'openid_connect' }
+
+ before do
+ prepare_provider_route('openid_connect')
+
+ mock_auth_hash(provider, extern_uid, user.email, additional_info: {})
+
+ request.env['devise.mapping'] = Devise.mappings[:user]
+ request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
+ end
+
+ it_behaves_like 'known sign in' do
+ let(:post_action) { post provider }
+ end
+
+ it 'allows sign in' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+
describe '#saml' do
let(:last_request_id) { 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685' }
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
@@ -431,8 +467,12 @@ 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])
+ stub_omniauth_saml_config(
+ enabled: true,
+ auto_link_saml_user: true,
+ allow_single_sign_on: ['saml'],
+ 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']
@@ -523,9 +563,10 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
expect(controller).to receive(:saml).and_wrap_original do |m, *args|
m.call(*args)
- expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'OmniauthCallbacksController#saml')
+ expect(Gitlab::ApplicationContext.current).to include(
+ 'meta.user' => user.username,
+ 'meta.caller_id' => 'OmniauthCallbacksController#saml'
+ )
end
post :saml, params: { SAMLResponse: mock_saml_response }
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index 9494f55c631..aad946acad4 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -99,8 +99,7 @@ RSpec.describe PasswordsController do
m.call(*args)
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'PasswordsController#update')
+ .to include('meta.user' => user.username, 'meta.caller_id' => 'PasswordsController#update')
end
subject
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 7d7cdededdb..dde0af3c543 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Profiles::TwoFactorAuthsController, feature_category: :authentication_and_authorization do
+RSpec.describe Profiles::TwoFactorAuthsController, feature_category: :system_access do
before do
# `user` should be defined within the action-specific describe blocks
sign_in(user)
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index daf0f36c28b..b1c43a33386 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -11,8 +11,7 @@ RSpec.describe ProfilesController, :request_store do
sign_in(user)
new_password = User.random_password
expect do
- post :update,
- params: { user: { password: new_password, password_confirmation: new_password } }
+ post :update, params: { user: { password: new_password, password_confirmation: new_password } }
end.not_to change { user.reload.encrypted_password }
expect(response).to have_gitlab_http_status(:found)
@@ -23,8 +22,7 @@ RSpec.describe ProfilesController, :request_store do
it 'allows an email update from a user without an external email address' do
sign_in(user)
- put :update,
- params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
+ put :update, params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
user.reload
@@ -37,8 +35,7 @@ RSpec.describe ProfilesController, :request_store do
create(:email, :confirmed, user: user, email: 'john@gmail.com')
sign_in(user)
- put :update,
- params: { user: { email: "john@gmail.com", name: "John" } }
+ put :update, params: { user: { email: "john@gmail.com", name: "John" } }
user.reload
@@ -54,8 +51,7 @@ RSpec.describe ProfilesController, :request_store do
ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true)
sign_in(ldap_user)
- put :update,
- params: { user: { email: "john@gmail.com", name: "John" } }
+ put :update, params: { user: { email: "john@gmail.com", name: "John" } }
ldap_user.reload
@@ -71,8 +67,7 @@ RSpec.describe ProfilesController, :request_store do
ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true, location_synced: false)
sign_in(ldap_user)
- put :update,
- params: { user: { email: "john@gmail.com", name: "John", location: "City, Country" } }
+ put :update, params: { user: { email: "john@gmail.com", name: "John", location: "City, Country" } }
ldap_user.reload
@@ -85,10 +80,7 @@ RSpec.describe ProfilesController, :request_store do
it 'allows setting a user status', :freeze_time do
sign_in(user)
- put(
- :update,
- params: { user: { status: { message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours' } } }
- )
+ 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')
@@ -183,22 +175,14 @@ RSpec.describe ProfilesController, :request_store do
end
it 'updates a username using JSON request' do
- put :update_username,
- params: {
- user: { username: new_username }
- },
- format: :json
+ put :update_username, params: { user: { username: new_username } }, format: :json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['message']).to eq(s_('Profiles|Username successfully changed'))
end
it 'renders an error message when the username was not updated' do
- put :update_username,
- params: {
- user: { username: 'invalid username.git' }
- },
- format: :json
+ put :update_username, params: { user: { username: 'invalid username.git' } }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to match(/Username change failed/)
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index c707b5dc39d..c91aa562a85 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -9,11 +9,13 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit.sha,
- ref: project.default_branch,
- status: 'success')
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: 'success'
+ )
end
let!(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
@@ -177,9 +179,10 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
end
it 'sends the codequality report' do
- expect(controller).to receive(:send_file)
- .with(job.job_artifacts_codequality.file.path,
- hash_including(disposition: 'attachment', filename: filename)).and_call_original
+ expect(controller).to receive(:send_file).with(
+ job.job_artifacts_codequality.file.path,
+ hash_including(disposition: 'attachment', filename: filename)
+ ).and_call_original
download_artifact(file_type: file_type)
@@ -557,8 +560,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
context 'with regular branch' do
before do
- pipeline.update!(ref: 'master',
- sha: project.commit('master').sha)
+ pipeline.update!(ref: 'master', sha: project.commit('master').sha)
get :latest_succeeded, params: params_from_ref('master')
end
@@ -568,8 +570,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
context 'with branch name containing slash' do
before do
- pipeline.update!(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
+ pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
get :latest_succeeded, params: params_from_ref('improve/awesome')
end
@@ -579,8 +580,7 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
context 'with branch name and path containing slashes' do
before do
- pipeline.update!(ref: 'improve/awesome',
- sha: project.commit('improve/awesome').sha)
+ pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
get :latest_succeeded, params: params_from_ref('improve/awesome', job.name, 'file/README.md')
end
@@ -596,11 +596,13 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
before do
create_file_in_repo(project, 'master', 'master', 'test.txt', 'This is test')
- create(:ci_pipeline,
+ create(
+ :ci_pipeline,
project: project,
sha: project.commit.sha,
ref: project.default_branch,
- status: 'failed')
+ status: 'failed'
+ )
get :latest_succeeded, params: params_from_ref(project.default_branch)
end
diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb
index d41e8d6169f..ef2afd7ca38 100644
--- a/spec/controllers/projects/badges_controller_spec.rb
+++ b/spec/controllers/projects/badges_controller_spec.rb
@@ -98,6 +98,16 @@ RSpec.describe Projects::BadgesController do
expect(response.body).to include('123')
end
end
+
+ if badge_type == :release
+ context 'when value_width param is used' do
+ it 'sets custom value width' do
+ get_badge(badge_type, value_width: '123')
+
+ expect(response.body).to include('123')
+ end
+ end
+ end
end
shared_examples 'a badge resource' do |badge_type|
@@ -186,7 +196,7 @@ RSpec.describe Projects::BadgesController do
namespace_id: project.namespace.to_param,
project_id: project,
ref: pipeline.ref
- }.merge(args.slice(:style, :key_text, :key_width, :ignore_skipped))
+ }.merge(args.slice(:style, :key_text, :key_width, :value_width, :ignore_skipped))
get badge, params: params, format: :svg
end
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index 62a544bb3fc..f322c78b5e3 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -17,12 +17,7 @@ RSpec.describe Projects::BlameController do
render_views
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
end
context "valid branch, valid file" do
@@ -35,8 +30,7 @@ RSpec.describe Projects::BlameController do
let(:id) { 'master/files/ruby/invalid-path.rb' }
it 'redirects' do
- expect(subject)
- .to redirect_to("/#{project.full_path}/-/tree/master")
+ expect(subject).to redirect_to("/#{project.full_path}/-/tree/master")
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 887a5ba598f..c091badd09d 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -75,13 +75,7 @@ RSpec.describe Projects::BlobController do
let(:id) { 'master/README.md' }
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- },
- format: :json)
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id }, format: :json
end
it do
@@ -95,14 +89,7 @@ RSpec.describe Projects::BlobController do
let(:id) { 'master/README.md' }
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id,
- viewer: 'none'
- },
- format: :json)
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id, viewer: 'none' }, format: :json
end
it do
@@ -115,12 +102,8 @@ RSpec.describe Projects::BlobController do
context 'with tree path' do
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
+
controller.instance_variable_set(:@blob, nil)
end
@@ -362,11 +345,23 @@ RSpec.describe Projects::BlobController do
end
end
- it_behaves_like 'tracking unique hll events' do
+ context 'events tracking' do
+ let(:target_event) { 'g_edit_by_sfe' }
+
subject(:request) { put :update, params: default_params }
- let(:target_event) { 'g_edit_by_sfe' }
- let(:expected_value) { instance_of(Integer) }
+ it_behaves_like 'tracking unique hll events' do
+ let(:expected_value) { instance_of(Integer) }
+ end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:action) { 'perform_sfe_action' }
+ let(:category) { described_class.to_s }
+ let(:namespace) { project.namespace.reload }
+ let(:property) { target_event }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_sfe_edit' }
+ let(:feature_flag_name) { 'route_hll_to_snowplow_phase4' }
+ end
end
end
@@ -494,6 +489,7 @@ RSpec.describe Projects::BlobController do
describe 'POST create' do
let(:user) { create(:user) }
+ let(:target_event) { 'g_edit_by_sfe' }
let(:default_params) do
{
namespace_id: project.namespace,
@@ -515,10 +511,18 @@ RSpec.describe Projects::BlobController do
subject(:request) { post :create, params: default_params }
it_behaves_like 'tracking unique hll events' do
- let(:target_event) { 'g_edit_by_sfe' }
let(:expected_value) { instance_of(Integer) }
end
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:action) { 'perform_sfe_action' }
+ let(:category) { described_class.to_s }
+ let(:namespace) { project.namespace }
+ let(:property) { target_event }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_sfe_edit' }
+ let(:feature_flag_name) { 'route_hll_to_snowplow_phase4' }
+ end
+
it 'redirects to blob' do
request
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index dcde22c1fd6..600f8047a1d 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -22,13 +22,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
before do
sign_in(developer)
- post :create,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- branch_name: branch,
- ref: ref
- }
+ post :create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ branch_name: branch,
+ ref: ref
+ }
end
context "valid branch name, valid source" do
@@ -83,13 +82,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
end
it 'redirects' do
- post :create,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
expect(subject)
.to redirect_to("/#{project.full_path}/-/tree/1-feature-branch")
@@ -98,13 +96,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
it 'posts a system note' do
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, developer, "1-feature-branch", branch_project: project)
- post :create,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
end
context 'confidential_issue_project_id is present' do
@@ -167,13 +164,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result)
expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
- post :create,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
expect(response).to redirect_to project_tree_path(project, branch)
end
@@ -189,13 +185,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result)
expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
- post :create,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
expect(response.location).to include(project_new_blob_path(project, branch))
expect(response).to have_gitlab_http_status(:found)
@@ -210,13 +205,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
expect_any_instance_of(::Branches::CreateService).to receive(:execute).and_return(result)
expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
- post :create,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
expect(response.location).to include(project_new_blob_path(project, branch))
expect(response).to have_gitlab_http_status(:found)
@@ -229,13 +223,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
it "doesn't post a system note" do
expect(SystemNoteService).not_to receive(:new_issue_branch)
- post :create,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
end
end
@@ -249,13 +242,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
it "doesn't post a system note" do
expect(SystemNoteService).not_to receive(:new_issue_branch)
- post :create,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- branch_name: branch,
- issue_iid: issue.iid
- }
+ post :create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ branch_name: branch,
+ issue_iid: issue.iid
+ }
end
end
end
@@ -285,18 +277,17 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
create_branch name: "<script>alert('merge');</script>", ref: "<script>alert('ref');</script>"
expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(response.body).to include 'Failed to create branch'
end
end
def create_branch(name:, ref:)
- post :create,
- format: :json,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: name,
- ref: ref
- }
+ post :create, format: :json, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: name,
+ ref: ref
+ }
end
end
@@ -345,13 +336,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
before do
sign_in(developer)
- post :destroy,
- format: format,
- params: {
- id: branch,
- namespace_id: project.namespace,
- project_id: project
- }
+ post :destroy, format: format, params: {
+ id: branch,
+ namespace_id: project.namespace,
+ project_id: project
+ }
end
context 'as JS' do
@@ -445,11 +434,10 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
describe "DELETE destroy_all_merged" do
def destroy_all_merged
- delete :destroy_all_merged,
- params: {
- namespace_id: project.namespace,
- project_id: project
- }
+ delete :destroy_all_merged, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
end
context 'when user is allowed to push' do
@@ -492,13 +480,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
context 'when rendering a JSON format' do
it 'filters branches by name' do
- get :index,
- format: :json,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- search: 'master'
- }
+ get :index, format: :json, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ search: 'master'
+ }
expect(json_response.length).to eq 1
expect(json_response.first).to eq 'master'
@@ -523,13 +509,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
status: :success,
created_at: 2.months.ago)
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- state: 'all'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ state: 'all'
+ }
expect(assigns[:branch_pipeline_statuses]["master"].group).to eq("success")
expect(assigns[:sort]).to eq('updated_desc')
@@ -555,13 +539,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
status: :success,
created_at: 2.months.ago)
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- state: 'all'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ state: 'all'
+ }
expect(assigns[:branch_pipeline_statuses]["master"].group).to eq("running")
expect(assigns[:branch_pipeline_statuses]["test"].group).to eq("success")
@@ -570,13 +552,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
context 'when a branch contains no pipelines' do
it 'no commit statuses are received' do
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- state: 'stale'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ state: 'stale'
+ }
expect(assigns[:branch_pipeline_statuses]).to be_blank
expect(assigns[:sort]).to eq('updated_asc')
@@ -589,14 +569,12 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
# was not raised whenever the cache is enabled yet cold.
context 'when cache is enabled yet cold', :request_store do
it 'return with a status 200' do
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- sort: 'name_asc',
- state: 'all'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ sort: 'name_asc',
+ state: 'all'
+ }
expect(response).to have_gitlab_http_status(:ok)
expect(assigns[:sort]).to eq('name_asc')
@@ -609,13 +587,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
end
it 'return with a status 200' do
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- state: 'all'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ state: 'all'
+ }
expect(response).to have_gitlab_http_status(:ok)
end
@@ -623,37 +599,31 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
context 'when deprecated sort/search/page parameters are specified' do
it 'returns with a status 301 when sort specified' do
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- sort: 'updated_asc'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ sort: 'updated_asc'
+ }
expect(response).to redirect_to project_branches_filtered_path(project, state: 'all')
end
it 'returns with a status 301 when search specified' do
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- search: 'feature'
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ search: 'feature'
+ }
expect(response).to redirect_to project_branches_filtered_path(project, state: 'all')
end
it 'returns with a status 301 when page specified' do
- get :index,
- format: :html,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- page: 2
- }
+ get :index, format: :html, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ page: 2
+ }
expect(response).to redirect_to project_branches_filtered_path(project, state: 'all')
end
@@ -747,13 +717,11 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
end
it 'returns the commit counts behind and ahead of default branch' do
- get :diverging_commit_counts,
- format: :json,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- names: %w[fix add-pdf-file branch-merged]
- }
+ get :diverging_commit_counts, format: :json, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ names: %w[fix add-pdf-file branch-merged]
+ }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq(
@@ -766,12 +734,10 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
it 'returns the commits counts with no names provided' do
allow_any_instance_of(Repository).to receive(:branch_count).and_return(Kaminari.config.default_per_page)
- get :diverging_commit_counts,
- format: :json,
- params: {
- namespace_id: project.namespace,
- project_id: project
- }
+ get :diverging_commit_counts, format: :json, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to be > 1
@@ -783,25 +749,21 @@ RSpec.describe Projects::BranchesController, feature_category: :source_code_mana
end
it 'returns 422 if no names are specified' do
- get :diverging_commit_counts,
- format: :json,
- params: {
- namespace_id: project.namespace,
- project_id: project
- }
+ get :diverging_commit_counts, format: :json, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['error']).to eq("Specify at least one and at most #{Kaminari.config.default_per_page} branch names")
end
it 'returns the list of counts' do
- get :diverging_commit_counts,
- format: :json,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- names: %w[fix add-pdf-file branch-merged]
- }
+ get :diverging_commit_counts, format: :json, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ names: %w[fix add-pdf-file branch-merged]
+ }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to be > 1
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index a4f7c92f5cd..38f72c769f3 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -420,11 +420,12 @@ RSpec.describe Projects::ClustersController, feature_category: :kubernetes_manag
describe 'PUT update' do
def go(format: :html)
- put :update, params: params.merge(namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: cluster,
- format: format
- )
+ put :update, params: params.merge(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: cluster,
+ format: format
+ )
end
before do
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 8d3939d8133..36206a88786 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::CommitController do
+RSpec.describe Projects::CommitController, feature_category: :source_code_management do
include ProjectForksHelper
let_it_be(:project) { create(:project, :repository) }
@@ -155,12 +155,7 @@ RSpec.describe Projects::CommitController do
let(:commit) { fork_project.commit('remove-submodule') }
it 'renders it' do
- get(:show,
- params: {
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- id: commit.id
- })
+ get :show, params: { namespace_id: fork_project.namespace, project_id: fork_project, id: commit.id }
expect(response).to be_successful
end
@@ -174,10 +169,10 @@ RSpec.describe Projects::CommitController do
go(id: commit.id, merge_request_iid: merge_request.iid)
expect(assigns(:new_diff_note_attrs)).to eq({
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id,
- commit_id: commit.id
- })
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ commit_id: commit.id
+ })
expect(response).to be_ok
end
end
@@ -187,12 +182,7 @@ RSpec.describe Projects::CommitController do
it 'contains branch and tags information' do
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- get(:branches,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: commit.id
- })
+ get :branches, params: { namespace_id: project.namespace, project_id: project, id: commit.id }
expect(assigns(:branches)).to include('master', 'feature_conflict')
expect(assigns(:branches_limit_exceeded)).to be_falsey
@@ -205,12 +195,7 @@ RSpec.describe Projects::CommitController do
allow_any_instance_of(Repository).to receive(:branch_count).and_return(1001)
allow_any_instance_of(Repository).to receive(:tag_count).and_return(1001)
- get(:branches,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: commit.id
- })
+ get :branches, params: { namespace_id: project.namespace, project_id: project, id: commit.id }
expect(assigns(:branches)).to eq([])
expect(assigns(:branches_limit_exceeded)).to be_truthy
@@ -234,12 +219,7 @@ RSpec.describe Projects::CommitController do
describe 'POST revert' do
context 'when target branch is not provided' do
it 'renders the 404 page' do
- post(:revert,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: commit.id
- })
+ post :revert, params: { namespace_id: project.namespace, project_id: project, id: commit.id }
expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(:not_found)
@@ -248,13 +228,7 @@ RSpec.describe Projects::CommitController do
context 'when the revert commit is missing' do
it 'renders the 404 page' do
- post(:revert,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: '1234567890'
- })
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: '1234567890' }
expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(:not_found)
@@ -263,13 +237,7 @@ RSpec.describe Projects::CommitController do
context 'when the revert was successful' do
it 'redirects to the commits page' do
- post(:revert,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: commit.id
- })
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: commit.id }
expect(response).to redirect_to project_commits_path(project, 'master')
expect(flash[:notice]).to eq('The commit has been successfully reverted.')
@@ -278,27 +246,53 @@ RSpec.describe Projects::CommitController do
context 'when the revert failed' do
before do
- post(:revert,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: commit.id
- })
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: commit.id }
end
it 'redirects to the commit page' do
# Reverting a commit that has been already reverted.
- post(:revert,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: commit.id
- })
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: commit.id }
expect(response).to redirect_to project_commit_path(project, commit.id)
- expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
+ expect(flash[:alert]).to match('Commit revert failed:')
+ end
+ end
+
+ context 'in the context of a merge_request' do
+ let(:merge_request) { create(:merge_request, :merged, source_project: project) }
+ let(:repository) { project.repository }
+
+ before do
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ 'Test message')
+
+ repository.commit(merge_commit_id)
+ merge_request.update!(merge_commit_sha: merge_commit_id)
+ end
+
+ context 'when the revert was successful' do
+ it 'redirects to the merge request page' do
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: merge_request.merge_commit_sha }
+
+ expect(response).to redirect_to project_merge_request_path(project, merge_request)
+ expect(flash[:notice]).to eq('The merge request has been successfully reverted.')
+ end
+ end
+
+ context 'when the revert failed' do
+ before do
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: merge_request.merge_commit_sha }
+ end
+
+ it 'redirects to the merge request page' do
+ # Reverting a merge request that has been already reverted.
+ post :revert, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: merge_request.merge_commit_sha }
+
+ expect(response).to redirect_to project_merge_request_path(project, merge_request)
+ expect(flash[:alert]).to match('Merge request revert failed:')
+ end
end
end
end
@@ -306,12 +300,7 @@ RSpec.describe Projects::CommitController do
describe 'POST cherry_pick' do
context 'when target branch is not provided' do
it 'renders the 404 page' do
- post(:cherry_pick,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: master_pickable_commit.id
- })
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, id: master_pickable_commit.id }
expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(:not_found)
@@ -320,13 +309,7 @@ RSpec.describe Projects::CommitController do
context 'when the cherry-pick commit is missing' do
it 'renders the 404 page' do
- post(:cherry_pick,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: '1234567890'
- })
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: '1234567890' }
expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(:not_found)
@@ -335,13 +318,7 @@ RSpec.describe Projects::CommitController do
context 'when the cherry-pick was successful' do
it 'redirects to the commits page' do
- post(:cherry_pick,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: master_pickable_commit.id
- })
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: master_pickable_commit.id }
expect(response).to redirect_to project_commits_path(project, 'master')
expect(flash[:notice]).to eq('The commit has been successfully cherry-picked into master.')
@@ -350,27 +327,52 @@ RSpec.describe Projects::CommitController do
context 'when the cherry_pick failed' do
before do
- post(:cherry_pick,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: master_pickable_commit.id
- })
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: master_pickable_commit.id }
end
it 'redirects to the commit page' do
# Cherry-picking a commit that has been already cherry-picked.
- post(:cherry_pick,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- start_branch: 'master',
- id: master_pickable_commit.id
- })
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'master', id: master_pickable_commit.id }
expect(response).to redirect_to project_commit_path(project, master_pickable_commit.id)
- expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
+ expect(flash[:alert]).to match('Commit cherry-pick failed:')
+ end
+ end
+
+ context 'in the context of a merge_request' do
+ let(:merge_request) { create(:merge_request, :merged, source_project: project) }
+ let(:repository) { project.repository }
+
+ before do
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ 'Test message')
+ repository.commit(merge_commit_id)
+ merge_request.update!(merge_commit_sha: merge_commit_id)
+ end
+
+ context 'when the cherry_pick was successful' do
+ it 'redirects to the merge request page' do
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'merge-test', id: merge_request.merge_commit_sha }
+
+ expect(response).to redirect_to project_merge_request_path(project, merge_request)
+ expect(flash[:notice]).to eq('The merge request has been successfully cherry-picked into merge-test.')
+ end
+ end
+
+ context 'when the cherry_pick failed' do
+ before do
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'merge-test', id: merge_request.merge_commit_sha }
+ end
+
+ it 'redirects to the merge request page' do
+ # Reverting a merge request that has been already cherry-picked.
+ post :cherry_pick, params: { namespace_id: project.namespace, project_id: project, start_branch: 'merge-test', id: merge_request.merge_commit_sha }
+
+ expect(response).to redirect_to project_merge_request_path(project, merge_request)
+ expect(flash[:alert]).to match('Merge request cherry-pick failed:')
+ end
end
end
@@ -381,15 +383,14 @@ RSpec.describe Projects::CommitController do
let(:create_merge_request) { nil }
def send_request
- post(:cherry_pick,
- params: {
- namespace_id: forked_project.namespace,
- project_id: forked_project,
- target_project_id: target_project.id,
- start_branch: 'feature',
- id: forked_project.commit.id,
- create_merge_request: create_merge_request
- })
+ post :cherry_pick, params: {
+ namespace_id: forked_project.namespace,
+ project_id: forked_project,
+ target_project_id: target_project.id,
+ start_branch: 'feature',
+ id: forked_project.commit.id,
+ create_merge_request: create_merge_request
+ }
end
def merge_request_url(source_project, branch)
@@ -478,8 +479,7 @@ RSpec.describe Projects::CommitController do
diff_for_path(id: commit2.id, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
- expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'Commit',
- commit_id: commit2.id)
+ expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'Commit', commit_id: commit2.id)
end
it 'only renders the diffs for the path given' do
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 67aa82dacbb..9e03d1f315b 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -18,11 +18,7 @@ RSpec.describe Projects::CommitsController, feature_category: :source_code_manag
describe "GET commits_root" do
context "no ref is provided" do
it 'redirects to the default branch of the project' do
- get(:commits_root,
- params: {
- namespace_id: project.namespace,
- project_id: project
- })
+ get :commits_root, params: { namespace_id: project.namespace, project_id: project }
expect(response).to redirect_to project_commits_path(project)
end
@@ -34,12 +30,7 @@ RSpec.describe Projects::CommitsController, feature_category: :source_code_manag
context 'with file path' do
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
end
context "valid branch, valid file" do
@@ -78,13 +69,7 @@ RSpec.describe Projects::CommitsController, feature_category: :source_code_manag
offset: 0
).and_call_original
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id,
- limit: "foo"
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id, limit: "foo" }
expect(response).to be_successful
end
@@ -98,27 +83,45 @@ RSpec.describe Projects::CommitsController, feature_category: :source_code_manag
offset: 0
).and_call_original
- get(:show, params: {
+ get :show, params: {
namespace_id: project.namespace,
project_id: project,
id: id,
limit: { 'broken' => 'value' }
- })
+ }
expect(response).to be_successful
end
end
end
+ it 'loads tags for commits' do
+ expect_next_instance_of(CommitCollection) do |collection|
+ expect(collection).to receive(:load_tags)
+ end
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: 'master/README.md' }
+ end
+
+ context 'when the show_tags_on_commits_view flag is disabled' do
+ let(:id) { "master/README.md" }
+
+ before do
+ stub_feature_flags(show_tags_on_commits_view: false)
+ end
+
+ it 'does not load tags' do
+ expect_next_instance_of(CommitCollection) do |collection|
+ expect(collection).not_to receive(:load_tags)
+ end
+
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
+ end
+ end
+
context "when the ref name ends in .atom" do
context "when the ref does not exist with the suffix" do
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: "master.atom"
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: "master.atom" }
end
it "renders as atom" do
@@ -138,12 +141,11 @@ RSpec.describe Projects::CommitsController, feature_category: :source_code_manag
allow_any_instance_of(Repository).to receive(:commit).and_call_original
allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: "master.atom"
- })
+ get :show, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: "master.atom"
+ }
end
it "renders as HTML" do
@@ -182,13 +184,11 @@ RSpec.describe Projects::CommitsController, feature_category: :source_code_manag
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original unless id.include?(' ')
- get(:signatures,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- },
- format: :json)
+ get :signatures, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id
+ }, format: :json
end
context "valid branch" do
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 034e6104f99..4ff8c21706b 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -15,11 +15,7 @@ RSpec.describe Projects::CycleAnalyticsController do
it 'increases the counter' do
expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views)
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project }
expect(response).to be_successful
end
@@ -35,7 +31,6 @@ RSpec.describe Projects::CycleAnalyticsController 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 }
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index ec63bad22b5..52a605cf548 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -276,9 +276,9 @@ RSpec.describe Projects::DeployKeysController do
let(:extra_params) { {} }
subject do
- put :update, params: extra_params.reverse_merge(id: deploy_key.id,
- namespace_id: project.namespace,
- project_id: project)
+ put :update, params: extra_params.reverse_merge(
+ id: deploy_key.id, namespace_id: project.namespace, project_id: project
+ )
end
def deploy_key_params(title, can_push)
@@ -330,9 +330,7 @@ RSpec.describe Projects::DeployKeysController do
context 'when a different deploy key id param is injected' do
let(:extra_params) { deploy_key_params('updated title', '1') }
let(:hacked_params) do
- extra_params.reverse_merge(id: other_deploy_key_id,
- namespace_id: project.namespace,
- project_id: project)
+ extra_params.reverse_merge(id: other_deploy_key_id, namespace_id: project.namespace, project_id: project)
end
subject { put :update, params: hacked_params }
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index c6532e83441..a696eb933e9 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -210,8 +210,6 @@ RSpec.describe Projects::DeploymentsController do
end
def deployment_params(opts = {})
- opts.reverse_merge(namespace_id: project.namespace,
- project_id: project,
- environment_id: environment.id)
+ opts.reverse_merge(namespace_id: project.namespace, project_id: project, environment_id: environment.id)
end
end
diff --git a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
index 5cc6e1b1bb4..1bb5112681c 100644
--- a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
+++ b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
@@ -139,10 +139,13 @@ RSpec.describe Projects::DesignManagement::Designs::ResizedImageController, feat
let(:sha) { newest_version.sha }
before do
- create(:design, :with_smaller_image_versions,
- issue: create(:issue, project: project),
- versions_count: 1,
- versions_sha: sha)
+ create(
+ :design,
+ :with_smaller_image_versions,
+ issue: create(:issue, project: project),
+ versions_count: 1,
+ versions_sha: sha
+ )
end
it 'serves the newest image' do
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 169fed1ab17..cbf632bfdb0 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -44,17 +44,9 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
allow_any_instance_of(Environment).to receive(:has_terminals?).and_return(true)
allow_any_instance_of(Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
- create(:environment, project: project,
- name: 'staging/review-1',
- state: :available)
-
- create(:environment, project: project,
- name: 'staging/review-2',
- state: :available)
-
- create(:environment, project: project,
- name: 'staging/review-3',
- state: :stopped)
+ create(:environment, project: project, name: 'staging/review-1', state: :available)
+ create(:environment, project: project, name: 'staging/review-2', state: :available)
+ create(:environment, project: project, name: 'staging/review-3', state: :stopped)
end
let(:environments) { json_response['environments'] }
@@ -84,9 +76,7 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
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(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
@@ -96,9 +86,7 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
get :index, params: environment_params(format: :json, search: 'review')
- expect(environments.map { |env| env['name'] }).to contain_exactly('review-app',
- 'staging/review-1',
- 'staging/review-2')
+ expect(environments.map { |env| env['name'] }).to contain_exactly('review-app', 'staging/review-1', 'staging/review-2')
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
@@ -245,23 +233,18 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
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)
+ 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,
- project_id: project,
- id: 'staging-1.0'
- },
- format: :json
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'staging-1.0'
+ }, format: :json
expect(response).to be_ok
expect(response).not_to render_template 'folder'
@@ -1016,98 +999,8 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
end
end
- describe '#append_info_to_payload' do
- let(:search_param) { 'my search param' }
-
- context 'when search_environment_logging feature is disabled' do
- before do
- stub_feature_flags(environments_search_logging: false)
- end
-
- it 'does not log search params in meta.environment.search' do
- expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload|
- method.call(payload)
-
- expect(payload[:metadata]).not_to have_key('meta.environment.search')
- expect(payload[:action]).to eq("search")
- expect(payload[:controller]).to eq("Projects::EnvironmentsController")
- end
-
- get :search, params: environment_params(format: :json, search: search_param)
- end
-
- it 'logs params correctly when search params are missing' do
- expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload|
- method.call(payload)
-
- expect(payload[:metadata]).not_to have_key('meta.environment.search')
- expect(payload[:action]).to eq("search")
- expect(payload[:controller]).to eq("Projects::EnvironmentsController")
- end
-
- get :search, params: environment_params(format: :json)
- end
-
- it 'logs params correctly when search params is empty string' do
- expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload|
- method.call(payload)
-
- expect(payload[:metadata]).not_to have_key('meta.environment.search')
- expect(payload[:action]).to eq("search")
- expect(payload[:controller]).to eq("Projects::EnvironmentsController")
- end
-
- get :search, params: environment_params(format: :json, search: "")
- end
- end
-
- context 'when search_environment_logging feature is enabled' do
- before do
- stub_feature_flags(environments_search_logging: true)
- end
-
- it 'logs search params in meta.environment.search' do
- expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload|
- method.call(payload)
-
- expect(payload[:metadata]['meta.environment.search']).to eq(search_param)
- expect(payload[:action]).to eq("search")
- expect(payload[:controller]).to eq("Projects::EnvironmentsController")
- end
-
- get :search, params: environment_params(format: :json, search: search_param)
- end
-
- it 'logs params correctly when search params are missing' do
- expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload|
- method.call(payload)
-
- expect(payload[:metadata]).not_to have_key('meta.environment.search')
- expect(payload[:action]).to eq("search")
- expect(payload[:controller]).to eq("Projects::EnvironmentsController")
- end
-
- get :search, params: environment_params(format: :json)
- end
-
- it 'logs params correctly when search params is empty string' do
- expect(controller).to receive(:append_info_to_payload).and_wrap_original do |method, payload|
- method.call(payload)
-
- expect(payload[:metadata]).not_to have_key('meta.environment.search')
- expect(payload[:action]).to eq("search")
- expect(payload[:controller]).to eq("Projects::EnvironmentsController")
- end
-
- get :search, params: environment_params(format: :json, search: "")
- end
- end
- end
-
def environment_params(opts = {})
- opts.reverse_merge(namespace_id: project.namespace,
- project_id: project,
- id: environment.id)
+ opts.reverse_merge(namespace_id: project.namespace, project_id: project, id: environment.id)
end
def additional_metrics(opts = {})
diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb
index 29ad51d590f..ac2e4233709 100644
--- a/spec/controllers/projects/feature_flags_controller_spec.rb
+++ b/spec/controllers/projects/feature_flags_controller_spec.rb
@@ -193,8 +193,7 @@ RSpec.describe Projects::FeatureFlagsController do
it 'routes based on iid' do
other_project = create(:project)
other_project.add_developer(user)
- other_feature_flag = create(:operations_feature_flag, project: other_project,
- name: 'other_flag')
+ other_feature_flag = create(:operations_feature_flag, project: other_project, name: 'other_flag')
params = {
namespace_id: other_project.namespace,
project_id: other_project,
@@ -485,8 +484,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')
+ create(:operations_feature_flag_user_list, project: project, name: 'My List', user_xids: 'user1,user2')
end
let(:params) do
@@ -627,10 +625,7 @@ RSpec.describe Projects::FeatureFlagsController do
context 'with a version 2 feature flag' do
let!(:new_version_flag) do
- create(:operations_feature_flag,
- name: 'new-feature',
- active: true,
- project: project)
+ create(:operations_feature_flag, name: 'new-feature', active: true, project: project)
end
it 'creates a new strategy and scope' do
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index a6c71cff74b..68810bae368 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -18,12 +18,7 @@ RSpec.describe Projects::FindFileController do
render_views
before do
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- })
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: id }
end
context "valid branch" do
@@ -41,13 +36,7 @@ RSpec.describe Projects::FindFileController do
describe "GET #list" do
def go(format: 'json')
- get :list,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- },
- format: format
+ get :list, params: { namespace_id: project.namespace, project_id: project, id: id }, format: format
end
context "valid branch" do
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 25c722173c1..3ea7054a64c 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -168,12 +168,7 @@ RSpec.describe Projects::ForksController, feature_category: :source_code_managem
let(:format) { :html }
subject(:do_request) do
- get :new,
- format: format,
- params: {
- namespace_id: project.namespace,
- project_id: project
- }
+ get :new, format: format, params: { namespace_id: project.namespace, project_id: project }
end
context 'when user is signed in' do
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
index 90ab49f9467..ae863918d14 100644
--- a/spec/controllers/projects/grafana_api_controller_spec.rb
+++ b/spec/controllers/projects/grafana_api_controller_spec.rb
@@ -87,13 +87,15 @@ RSpec.describe Projects::GrafanaApiController, feature_category: :metrics do
it 'returns a grafana datasource response' do
get :proxy, params: params
- expect(Grafana::ProxyService)
- .to have_received(:new)
- .with(project, '1', 'api/v1/query_range',
- { 'query' => params[:query],
- 'start' => params[:start_time],
- 'end' => params[:end_time],
- 'step' => params[:step] })
+ expect(Grafana::ProxyService).to have_received(:new).with(
+ project, '1', 'api/v1/query_range',
+ {
+ 'query' => params[:query],
+ 'start' => params[:start_time],
+ '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 1e9d999311a..3e5bcbbc9ba 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -141,7 +141,6 @@ RSpec.describe Projects::GraphsController do
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 }
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index a5c00d24e30..2075dd3e7a7 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GroupLinksController, feature_category: :authentication_and_authorization do
+RSpec.describe Projects::GroupLinksController, feature_category: :system_access do
let_it_be(:group) { create(:group, :private) }
let_it_be(:group2) { create(:group, :private) }
let_it_be(:project) { create(:project, :private, group: group2) }
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 815370d428d..c056e7a33aa 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HooksController do
+RSpec.describe Projects::HooksController, feature_category: :integrations do
include AfterNextHelpers
let_it_be(:project) { create(:project) }
@@ -173,6 +173,16 @@ RSpec.describe Projects::HooksController do
let(:params) { { namespace_id: project.namespace, project_id: project, id: hook } }
it_behaves_like 'Web hook destroyer'
+
+ context 'when user does not have permission' do
+ let(:user) { create(:user, developer_projects: [project]) }
+
+ it 'renders a 404' do
+ delete :destroy, params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
describe '#test' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 9c272872a73..f1fe1940414 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -183,18 +183,6 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
let_it_be(:task) { create(:issue, :task, project: project) }
shared_examples 'redirects to show work item page' do
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'redirects to work item page' do
- make_request
-
- expect(response).to redirect_to(project_work_items_path(project, task.id, query))
- end
- end
-
it 'redirects to work item page using iid' do
make_request
@@ -585,15 +573,13 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
end
def reorder_issue(issue, move_after_id: nil, move_before_id: nil)
- put :reorder,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: issue.iid,
- move_after_id: move_after_id,
- move_before_id: move_before_id
- },
- format: :json
+ put :reorder, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: issue.iid,
+ move_after_id: move_after_id,
+ move_before_id: move_before_id
+ }, format: :json
end
end
@@ -601,14 +587,12 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
let(:issue_params) { { title: 'New title' } }
subject do
- put :update,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: issue.to_param,
- issue: issue_params
- },
- format: :json
+ put :update, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.to_param,
+ issue: issue_params
+ }, format: :json
end
before do
@@ -1927,12 +1911,11 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
end
it 'redirects from an old issue/designs correctly' do
- get :designs,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: issue
- }
+ get :designs, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue
+ }
expect(response).to redirect_to(designs_project_issue_path(new_project, issue))
expect(response).to have_gitlab_http_status(:moved_permanently)
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 2d047957430..2e29d87dadd 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -106,9 +106,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
def create_job(name, status)
user = create(:user)
pipeline = create(:ci_pipeline, project: project, user: user)
- create(:ci_build, :tags, :triggered, :artifacts,
- pipeline: pipeline, name: name, status: status,
- user: user)
+ create(
+ :ci_build, :tags, :triggered, :artifacts,
+ pipeline: pipeline, name: name, status: status, user: user
+ )
end
end
@@ -832,8 +833,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
retried_build = Ci::Build.last
Ci::Build.clone_accessors.each do |accessor|
- expect(job.read_attribute(accessor))
- .to eq(retried_build.read_attribute(accessor)),
+ expect(job.read_attribute(accessor)).to eq(retried_build.read_attribute(accessor)),
"Mismatched attribute on \"#{accessor}\". " \
"It was \"#{job.read_attribute(accessor)}\" but changed to \"#{retried_build.read_attribute(accessor)}\""
end
@@ -855,10 +855,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
def post_retry
post :retry, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: job.id
- }
+ namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
+ }
end
end
@@ -869,8 +869,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
before do
project.add_developer(user)
- create(:protected_branch, :developers_can_merge,
- name: 'protected-branch', project: project)
+ create(:protected_branch, :developers_can_merge, name: 'protected-branch', project: project)
sign_in(user)
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 19a04654114..b5092a0f091 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -19,11 +19,10 @@ RSpec.describe Projects::MattermostsController do
end
it 'accepts the request' do
- get(:new,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- })
+ get :new, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
expect(response).to have_gitlab_http_status(:ok)
end
@@ -33,12 +32,11 @@ RSpec.describe Projects::MattermostsController do
let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } }
subject do
- post(:create,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- mattermost: mattermost_params
- })
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ mattermost: mattermost_params
+ }
end
context 'no request can be made to mattermost' do
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 311af26abf6..356741fc4e2 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -22,13 +22,11 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_loading_conflict_ui_action)
- get :show,
- params: {
- namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project,
- id: merge_request_with_conflicts.iid
- },
- format: 'html'
+ get :show, params: {
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project,
+ id: merge_request_with_conflicts.iid
+ }, format: 'html'
end
it 'does tracks the resolve call' do
@@ -45,13 +43,11 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
.and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
- get :show,
- params: {
- namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project,
- id: merge_request_with_conflicts.iid
- },
- format: 'json'
+ get :show, params: {
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project,
+ id: merge_request_with_conflicts.iid
+ }, format: 'json'
end
it 'returns a 200 status code' do
@@ -70,13 +66,11 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
context 'with valid conflicts' do
before do
- get :show,
- params: {
- namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project,
- id: merge_request_with_conflicts.iid
- },
- format: 'json'
+ get :show, params: {
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project,
+ id: merge_request_with_conflicts.iid
+ }, format: 'json'
end
it 'matches the schema' do
@@ -130,15 +124,13 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
describe 'GET conflict_for_path' do
def conflict_for_path(path)
- get :conflict_for_path,
- params: {
- namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project,
- id: merge_request_with_conflicts.iid,
- old_path: path,
- new_path: path
- },
- format: 'json'
+ get :conflict_for_path, params: {
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project,
+ id: merge_request_with_conflicts.iid,
+ old_path: path,
+ new_path: path
+ }, format: 'json'
end
context 'when the conflicts cannot be resolved in the UI' do
@@ -178,11 +170,13 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include('old_path' => path,
- 'new_path' => path,
- 'blob_icon' => 'doc-text',
- 'blob_path' => a_string_ending_with(path),
- 'content' => content)
+ expect(json_response).to include(
+ 'old_path' => path,
+ 'new_path' => path,
+ 'blob_icon' => 'doc-text',
+ 'blob_path' => a_string_ending_with(path),
+ 'content' => content
+ )
end
end
end
@@ -197,15 +191,13 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
end
def resolve_conflicts(files)
- post :resolve_conflicts,
- params: {
- namespace_id: merge_request_with_conflicts.project.namespace.to_param,
- project_id: merge_request_with_conflicts.project,
- id: merge_request_with_conflicts.iid,
- files: files,
- commit_message: 'Commit message'
- },
- format: 'json'
+ post :resolve_conflicts, params: {
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project,
+ id: merge_request_with_conflicts.iid,
+ files: files,
+ commit_message: 'Commit message'
+ }, format: 'json'
end
context 'with valid params' do
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index 3d4a884587f..c6a4dcbfdf0 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -99,9 +99,7 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
describe 'GET pipelines' do
before do
- create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id,
- ref: 'remove-submodule',
- project: fork_project)
+ create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id, ref: 'remove-submodule', project: fork_project)
end
it 'renders JSON including serialized pipelines' do
@@ -188,13 +186,12 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { true }.at_least(:once)
- get :branch_to,
- params: {
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id,
- ref: 'master'
- }
+ get :branch_to, params: {
+ namespace_id: fork_project.namespace,
+ project_id: fork_project,
+ target_project_id: project.id,
+ ref: 'master'
+ }
expect(assigns(:commit)).not_to be_nil
expect(response).to have_gitlab_http_status(:ok)
@@ -204,13 +201,12 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { false }.at_least(:once)
- get :branch_to,
- params: {
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id,
- ref: 'master'
- }
+ get :branch_to, params: {
+ namespace_id: fork_project.namespace,
+ project_id: fork_project,
+ target_project_id: project.id,
+ ref: 'master'
+ }
expect(assigns(:commit)).to be_nil
expect(response).to have_gitlab_http_status(:ok)
@@ -220,13 +216,12 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
expect(Ability).to receive(:allowed?).with(user, :create_merge_request_in, project) { true }.at_least(:once)
- get :branch_to,
- params: {
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id,
- ref: 'master'
- }
+ get :branch_to, params: {
+ namespace_id: fork_project.namespace,
+ project_id: fork_project,
+ target_project_id: project.id,
+ ref: 'master'
+ }
expect(assigns(:commit)).to be_nil
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 23a33d7e0b1..a5dc351201d 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -247,9 +247,11 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code
straight: true)
end
- go(diff_head: true,
- diff_id: merge_request.merge_request_diff.id,
- start_sha: merge_request.merge_request_diff.start_commit_sha)
+ go(
+ diff_head: true,
+ diff_id: merge_request.merge_request_diff.id,
+ start_sha: merge_request.merge_request_diff.start_commit_sha
+ )
end
end
end
@@ -329,9 +331,11 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code
diff_for_path(old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_falsey
- expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest',
- noteable_id: merge_request.id,
- commit_id: nil)
+ expect(assigns(:new_diff_note_attrs)).to eq(
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ commit_id: nil
+ )
end
it 'only renders the diffs for the path given' do
@@ -528,8 +532,7 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code
context 'with diff_id and start_sha params' do
subject do
- go(diff_id: merge_request.merge_request_diff.id,
- start_sha: merge_request.merge_request_diff.start_commit_sha)
+ go(diff_id: merge_request.merge_request_diff.id, start_sha: merge_request.merge_request_diff.start_commit_sha)
end
it_behaves_like 'serializes diffs with expected arguments' do
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 39482938a8b..6632473a85c 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -299,8 +299,7 @@ RSpec.describe Projects::MergeRequests::DraftsController do
it 'publishes a draft note with quick actions and applies them', :sidekiq_inline do
project.add_developer(user2)
- create(:draft_note, merge_request: merge_request, author: user,
- note: "/assign #{user2.to_reference}")
+ create(:draft_note, merge_request: merge_request, author: user, note: "/assign #{user2.to_reference}")
expect(merge_request.assignees).to be_empty
@@ -350,12 +349,13 @@ RSpec.describe Projects::MergeRequests::DraftsController do
let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
def create_reply(discussion_id, resolves: false)
- create(:draft_note,
- merge_request: merge_request,
- author: user,
- discussion_id: discussion_id,
- resolve_discussion: resolves
- )
+ create(
+ :draft_note,
+ merge_request: merge_request,
+ author: user,
+ discussion_id: discussion_id,
+ resolve_discussion: resolves
+ )
end
it 'resolves a thread if the draft note resolves it' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index ceb3f803db5..9e18089bb23 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -210,9 +210,7 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
diff = merge_request.merge_request_diff
diff.clean!
- diff.update!(real_size: nil,
- start_commit_sha: nil,
- base_commit_sha: nil)
+ diff.update!(real_size: nil, start_commit_sha: nil, base_commit_sha: nil)
go(format: :html)
@@ -270,24 +268,22 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
it 'redirects from an old merge request correctly' do
- get :show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request
- }
+ get :show, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request
+ }
expect(response).to redirect_to(project_merge_request_path(new_project, merge_request))
expect(response).to have_gitlab_http_status(:moved_permanently)
end
it 'redirects from an old merge request commits correctly' do
- get :commits,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request
- }
+ get :commits, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request
+ }
expect(response).to redirect_to(commits_project_merge_request_path(new_project, merge_request))
expect(response).to have_gitlab_http_status(:moved_permanently)
@@ -385,13 +381,12 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
def get_merge_requests(page = nil)
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- state: 'opened',
- page: page.to_param
- }
+ get :index, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ state: 'opened',
+ page: page.to_param
+ }
end
it_behaves_like "issuables list meta-data", :merge_request
@@ -842,15 +837,13 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
describe 'GET commits' do
def go(page: nil, per_page: 1, format: 'html')
- get :commits,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- page: page,
- per_page: per_page
- },
- format: format
+ get :commits, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ page: page,
+ per_page: per_page
+ }, format: format
end
it 'renders the commits template to a string' do
@@ -884,17 +877,18 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
describe 'GET pipelines' do
before do
- create(:ci_pipeline, project: merge_request.source_project,
- ref: merge_request.source_branch,
- sha: merge_request.diff_head_sha)
+ create(
+ :ci_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
- get :pipelines,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ get :pipelines, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ }, format: :json
end
context 'with "enabled" builds on a public project' do
@@ -1955,17 +1949,18 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
let(:issue2) { create(:issue, project: project) }
def post_assign_issues
- merge_request.update!(description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
- author: user,
- source_branch: 'feature',
- target_branch: 'master')
+ merge_request.update!(
+ description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
+ author: user,
+ source_branch: 'feature',
+ target_branch: 'master'
+ )
- post :assign_related_issues,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid
- }
+ post :assign_related_issues, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ }
end
it 'displays an flash error message on fail' do
@@ -2143,10 +2138,13 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
describe 'GET pipeline_status.json' do
context 'when head_pipeline exists' do
let!(:pipeline) do
- create(:ci_pipeline, project: merge_request.source_project,
- ref: merge_request.source_branch,
- sha: merge_request.diff_head_sha,
- head_pipeline_of: merge_request)
+ create(
+ :ci_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ head_pipeline_of: merge_request
+ )
end
let(:status) { pipeline.detailed_status(double('user')) }
@@ -2199,11 +2197,10 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
def get_pipeline_status
get :pipeline_status, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid
- },
- format: :json
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid
+ }, format: :json
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 23b0b58158f..5e4e47be2c5 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -37,6 +37,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
project.add_developer(user)
end
+ specify { expect(get(:index, params: request_params)).to have_request_urgency(:medium) }
+
it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do
last_fetched_at = Time.zone.at(3.hours.ago.to_i) # remove nanoseconds
@@ -244,6 +246,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
sign_in(user)
end
+ specify { expect(create!).to have_request_urgency(:low) }
+
describe 'making the creation request' do
before do
create!
@@ -432,6 +436,13 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time')
expect(json_response['commands_changes']).not_to include('target_project', 'title')
end
+
+ it 'includes command_names' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['command_names']).to include('award', 'estimate', 'spend')
+ end
end
context 'with commands that do not return changes' do
@@ -450,6 +461,13 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['commands_changes']).not_to include('target_project', 'title')
end
+
+ it 'includes command_names' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['command_names']).to include('move', 'title')
+ end
end
end
end
@@ -484,10 +502,7 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
let(:commit) { create(:commit, project: project) }
let(:existing_comment) do
- create(:note_on_commit,
- note: 'first',
- project: project,
- commit_id: merge_request.commit_shas.first)
+ create(:note_on_commit, note: 'first', project: project, commit_id: merge_request.commit_shas.first)
end
let(:discussion) { existing_comment.discussion }
@@ -735,19 +750,21 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
end
describe 'PUT update' do
- context "should update the note with a valid issue" do
- let(:request_params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- id: note,
- format: :json,
- note: {
- note: "New comment"
- }
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :json,
+ note: {
+ note: "New comment"
}
- end
+ }
+ end
+
+ specify { expect(put(:update, params: request_params)).to have_request_urgency(:low) }
+ context "should update the note with a valid issue" do
before do
sign_in(note.author)
project.add_developer(note.author)
@@ -793,6 +810,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
}
end
+ specify { expect(delete(:destroy, params: request_params)).to have_request_urgency(:low) }
+
context 'user is the author of a note' do
before do
sign_in(note.author)
@@ -834,6 +853,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
let(:emoji_name) { 'thumbsup' }
+ it { is_expected.to have_request_urgency(:low) }
+
it "toggles the award emoji" do
expect do
subject
@@ -869,6 +890,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
sign_in user
end
+ specify { expect(post(:resolve, params: request_params)).to have_request_urgency(:low) }
+
context "when the user is not authorized to resolve the note" do
it "returns status 404" do
post :resolve, params: request_params
@@ -932,6 +955,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
note.resolve!(user)
end
+ specify { expect(delete(:unresolve, params: request_params)).to have_request_urgency(:low) }
+
context "when the user is not authorized to resolve the note" do
it "returns status 404" do
delete :unresolve, params: request_params
@@ -1001,6 +1026,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
expect(json_response.count).to eq(1)
expect(json_response.first).to include({ "line_text" => "Test" })
end
+
+ specify { expect(get(:outdated_line_change, params: request_params)).to have_request_urgency(:low) }
end
# Convert a time to an integer number of microseconds
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 136f98ac907..ded5dd57e3e 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PagesController do
+RSpec.describe Projects::PagesController, feature_category: :pages do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -14,7 +14,12 @@ RSpec.describe Projects::PagesController do
end
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_config(pages: {
+ enabled: true,
+ external_https: true,
+ access_control: false
+ })
+
sign_in(user)
project.add_maintainer(user)
end
@@ -123,49 +128,99 @@ RSpec.describe Projects::PagesController do
end
describe 'PATCH update' do
- let(:request_params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- project: { pages_https_only: 'false' }
- }
- end
+ context 'when updating pages_https_only' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: { pages_https_only: 'true' }
+ }
+ end
- let(:update_service) { double(execute: { status: :success }) }
+ it 'updates project field and redirects back to the pages settings' do
+ project.update!(pages_https_only: false)
- before do
- allow(Projects::UpdateService).to receive(:new) { update_service }
- end
+ expect { patch :update, params: request_params }
+ .to change { project.reload.pages_https_only }
+ .from(false).to(true)
- it 'returns 302 status' do
- patch :update, params: request_params
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(project_pages_path(project))
+ end
- expect(response).to have_gitlab_http_status(:found)
- end
+ context 'when it fails to update' do
+ it 'adds an error message' do
+ expect_next_instance_of(Projects::UpdateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return(status: :error, message: 'some error happened')
+ end
- it 'redirects back to the pages settings' do
- patch :update, params: request_params
+ expect { patch :update, params: request_params }
+ .not_to change { project.reload.pages_https_only }
- expect(response).to redirect_to(project_pages_path(project))
+ expect(response).to redirect_to(project_pages_path(project))
+ expect(flash[:alert]).to eq('some error happened')
+ end
+ end
end
- it 'calls the update service' do
- expect(Projects::UpdateService)
- .to receive(:new)
- .with(project, user, ActionController::Parameters.new(request_params[:project]).permit!)
- .and_return(update_service)
+ context 'when updating pages_unique_domain' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: {
+ project_setting_attributes: {
+ pages_unique_domain_enabled: 'true'
+ }
+ }
+ }
+ end
- patch :update, params: request_params
- end
+ before do
+ create(:project_setting, project: project, pages_unique_domain_enabled: false)
+ end
- context 'when update_service returns an error message' do
- let(:update_service) { double(execute: { status: :error, message: 'some error happened' }) }
+ context 'with pages_unique_domain feature flag disabled' do
+ it 'does not update pages unique domain' do
+ stub_feature_flags(pages_unique_domain: false)
- it 'adds an error message' do
- patch :update, params: request_params
+ expect { patch :update, params: request_params }
+ .not_to change { project.project_setting.reload.pages_unique_domain_enabled }
+ end
+ end
- expect(response).to redirect_to(project_pages_path(project))
- expect(flash[:alert]).to eq('some error happened')
+ context 'with pages_unique_domain feature flag enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ it 'updates pages_https_only and pages_unique_domain and redirects back to pages settings' do
+ expect { patch :update, params: request_params }
+ .to change { project.project_setting.reload.pages_unique_domain_enabled }
+ .from(false).to(true)
+
+ expect(project.project_setting.pages_unique_domain).not_to be_nil
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(project_pages_path(project))
+ end
+
+ context 'when it fails to update' do
+ it 'adds an error message' do
+ expect_next_instance_of(Projects::UpdateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return(status: :error, message: 'some error happened')
+ end
+
+ expect { patch :update, params: request_params }
+ .not_to change { project.project_setting.reload.pages_unique_domain_enabled }
+
+ expect(response).to redirect_to(project_pages_path(project))
+ expect(flash[:alert]).to eq('some error happened')
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index a628c1ab230..6d810fdcd51 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -410,9 +410,9 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
it { expect { go }.to be_denied_for(:visitor) }
context 'when user is schedule owner' do
- it { expect { go }.to be_denied_for(:owner).of(project).own(pipeline_schedule) }
- it { expect { go }.to be_denied_for(:maintainer).of(project).own(pipeline_schedule) }
- it { expect { go }.to be_denied_for(:developer).of(project).own(pipeline_schedule) }
+ it { expect { go }.to be_allowed_for(:owner).of(project).own(pipeline_schedule) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(project).own(pipeline_schedule) }
+ it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) }
it { expect { go }.to be_denied_for(:reporter).of(project).own(pipeline_schedule) }
it { expect { go }.to be_denied_for(:guest).of(project).own(pipeline_schedule) }
it { expect { go }.to be_denied_for(:user).own(pipeline_schedule) }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 4e0c098ad81..09b703a48d6 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -199,22 +199,36 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
check_pipeline_response(returned: 6, all: 6)
end
end
+
+ context "with lazy_load_pipeline_dropdown_actions feature flag disabled" do
+ before do
+ stub_feature_flags(lazy_load_pipeline_dropdown_actions: false)
+ end
+
+ it 'returns manual and scheduled actions' do
+ get_pipelines_index_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('pipeline')
+
+ expect(json_response.dig('pipelines', 0, 'details')).to include('manual_actions')
+ expect(json_response.dig('pipelines', 0, 'details')).to include('scheduled_actions')
+ end
+ end
end
def get_pipelines_index_html(params = {})
get :index, params: {
- namespace_id: project.namespace,
- project_id: project
- }.merge(params),
- format: :html
+ namespace_id: project.namespace,
+ project_id: project
+ }.merge(params), format: :html
end
def get_pipelines_index_json(params = {})
get :index, params: {
- namespace_id: project.namespace,
- project_id: project
- }.merge(params),
- format: :json
+ namespace_id: project.namespace,
+ project_id: project
+ }.merge(params), format: :json
end
def create_all_pipeline_types
@@ -236,12 +250,15 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
def create_pipeline(status, sha, merge_request: nil)
user = create(:user)
- pipeline = create(:ci_empty_pipeline, status: status,
- project: project,
- sha: sha.id,
- ref: sha.id.first(8),
- user: user,
- merge_request: merge_request)
+ pipeline = create(
+ :ci_empty_pipeline,
+ status: status,
+ project: project,
+ sha: sha.id,
+ ref: sha.id.first(8),
+ user: user,
+ merge_request: merge_request
+ )
build_stage = create(:ci_stage, name: 'build', pipeline: pipeline)
test_stage = create(:ci_stage, name: 'test', pipeline: pipeline)
@@ -378,9 +395,7 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
let(:project) { create(:project, :repository) }
let(:pipeline) do
- create(:ci_empty_pipeline, project: project,
- user: user,
- sha: project.commit.id)
+ create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id)
end
let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
@@ -598,9 +613,7 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
def create_pipeline(project)
create(:ci_empty_pipeline, project: project).tap do |pipeline|
- create(:ci_build, pipeline: pipeline,
- ci_stage: create(:ci_stage, name: 'test', pipeline: pipeline),
- name: 'rspec')
+ create(:ci_build, pipeline: pipeline, ci_stage: create(:ci_stage, name: 'test', pipeline: pipeline), name: 'rspec')
end
end
@@ -771,11 +784,8 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
before do
get :status, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: pipeline.id
- },
- format: :json
+ namespace_id: project.namespace, project_id: project, id: pipeline.id
+ }, format: :json
end
it 'return a detailed pipeline status in json' do
@@ -825,7 +835,6 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
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 }
@@ -868,9 +877,7 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
context 'when latest commit contains [ci skip]' do
before do
- project.repository.create_file(user, 'new-file.txt', 'A new file',
- message: '[skip ci] This is a test',
- branch_name: 'master')
+ project.repository.create_file(user, 'new-file.txt', 'A new file', message: '[skip ci] This is a test', branch_name: 'master')
end
it_behaves_like 'creates a pipeline'
@@ -906,11 +913,8 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
subject do
post :create, params: {
- namespace_id: project.namespace,
- project_id: project,
- pipeline: { ref: 'master' }
- },
- format: :json
+ namespace_id: project.namespace, project_id: project, pipeline: { ref: 'master' }
+ }, format: :json
end
before do
@@ -969,11 +973,8 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
describe 'POST retry.json' do
subject(:post_retry) do
post :retry, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: pipeline.id
- },
- format: :json
+ namespace_id: project.namespace, project_id: project, id: pipeline.id
+ }, format: :json
end
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
@@ -1036,11 +1037,8 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
before do
post :cancel, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: pipeline.id
- },
- format: :json
+ namespace_id: project.namespace, project_id: project, id: pipeline.id
+ }, format: :json
end
it 'cancels a pipeline without returning any content', :sidekiq_might_not_need_inline do
@@ -1192,17 +1190,11 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
let(:branch_secondary) { project.repository.branches[1] }
let!(:pipeline_master) do
- create(:ci_pipeline,
- ref: branch_main.name,
- sha: branch_main.target,
- project: project)
+ create(:ci_pipeline, ref: branch_main.name, sha: branch_main.target, project: project)
end
let!(:pipeline_secondary) do
- create(:ci_pipeline,
- ref: branch_secondary.name,
- sha: branch_secondary.target,
- project: project)
+ create(:ci_pipeline, ref: branch_secondary.name, sha: branch_secondary.target, project: project)
end
before do
@@ -1455,10 +1447,9 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
private
def get_config_variables
- get :config_variables, params: { namespace_id: project.namespace,
- project_id: project,
- sha: ref },
- format: :json
+ get :config_variables, params: {
+ namespace_id: project.namespace, project_id: project, sha: ref
+ }, format: :json
end
end
diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
index 09b9f25c0c6..91d3ba7e106 100644
--- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
@@ -117,10 +117,7 @@ RSpec.describe Projects::Prometheus::AlertsController do
describe 'GET #metrics_dashboard' do
let!(:alert) do
- create(:prometheus_alert,
- project: project,
- environment: environment,
- prometheus_metric: metric)
+ create(:prometheus_alert, project: project, environment: environment, prometheus_metric: metric)
end
it 'returns a json object with the correct keys' do
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 40252cf65cd..b15a37d8d90 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -12,13 +12,9 @@ RSpec.describe Projects::RawController, feature_category: :source_code_managemen
describe 'GET #show' do
def get_show
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: file_path,
- inline: inline
- }.merge(params))
+ get :show, params: {
+ namespace_id: project.namespace, project_id: project, id: file_path, inline: inline
+ }.merge(params)
end
subject { get_show }
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index a0d119baf16..0b1d0b75de7 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -54,14 +54,9 @@ RSpec.describe Projects::RefsController, feature_category: :source_code_manageme
let(:path) { 'foo/bar/baz.html' }
def default_get(format = :html)
- get :logs_tree,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: 'master',
- path: path
- },
- format: format
+ get :logs_tree, params: {
+ namespace_id: project.namespace.to_param, project_id: project, id: 'master', path: path
+ }, format: format
end
def xhr_get(format = :html, params = {})
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index 59bc1ba04e7..834fdddd583 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -59,8 +59,7 @@ RSpec.describe Projects::Registry::RepositoriesController do
context 'when root container repository is not created' do
context 'when there are tags for this repository' do
before do
- stub_container_registry_tags(repository: :any,
- tags: %w[rc1 latest])
+ stub_container_registry_tags(repository: :any, tags: %w[rc1 latest])
end
it 'creates a root container repository' do
@@ -139,19 +138,12 @@ RSpec.describe Projects::Registry::RepositoriesController do
end
def go_to_index(format: :html, params: {})
- get :index, params: params.merge({
- namespace_id: project.namespace,
- project_id: project
- }),
- format: format
+ get :index, params: params.merge({ namespace_id: project.namespace, project_id: project }), format: format
end
def delete_repository(repository)
delete :destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: repository
- },
- format: :json
+ namespace_id: project.namespace, project_id: project, id: repository
+ }, format: :json
end
end
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index 7b786f4a8af..afa7bd6a60d 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -76,11 +76,8 @@ RSpec.describe Projects::Registry::TagsController do
def get_tags
get :index, params: {
- namespace_id: project.namespace,
- project_id: project,
- repository_id: repository
- },
- format: :json
+ namespace_id: project.namespace, project_id: project, repository_id: repository
+ }, format: :json
end
end
@@ -121,12 +118,11 @@ RSpec.describe Projects::Registry::TagsController do
def destroy_tag(name)
post :destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- repository_id: repository,
- id: name
- },
- format: :json
+ namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ id: name
+ }, format: :json
end
end
@@ -162,12 +158,11 @@ RSpec.describe Projects::Registry::TagsController do
def bulk_destroy_tags(names)
post :bulk_destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- repository_id: repository,
- ids: names
- },
- format: :json
+ namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ ids: names
+ }, format: :json
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index ba917fa3a31..1c332eadc42 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -173,12 +173,11 @@ RSpec.describe Projects::Settings::CiCdController, feature_category: :continuous
let(:params) { { ci_config_path: '' } }
subject do
- patch :update,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- project: params
- }
+ patch :update, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ project: params
+ }
end
it 'redirects to the settings page' do
@@ -241,9 +240,7 @@ RSpec.describe Projects::Settings::CiCdController, feature_category: :continuous
end
it 'creates a pipeline', :sidekiq_inline do
- project.repository.create_file(user, 'Gemfile', 'Gemfile contents',
- message: 'Add Gemfile',
- branch_name: 'master')
+ project.repository.create_file(user, 'Gemfile', 'Gemfile contents', message: 'Add Gemfile', branch_name: 'master')
expect { subject }.to change { Ci::Pipeline.count }.by(1)
end
diff --git a/spec/controllers/projects/settings/merge_requests_controller_spec.rb b/spec/controllers/projects/settings/merge_requests_controller_spec.rb
index 106ec62bea0..398fc97a00d 100644
--- a/spec/controllers/projects/settings/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/settings/merge_requests_controller_spec.rb
@@ -36,12 +36,11 @@ RSpec.describe Projects::Settings::MergeRequestsController do
merge_method: :ff
}
- put :update,
- params: {
- namespace_id: project.namespace,
- project_id: project.id,
- project: params
- }
+ 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|
diff --git a/spec/controllers/projects/snippets/blobs_controller_spec.rb b/spec/controllers/projects/snippets/blobs_controller_spec.rb
index ca656705e07..4d12452e3d5 100644
--- a/spec/controllers/projects/snippets/blobs_controller_spec.rb
+++ b/spec/controllers/projects/snippets/blobs_controller_spec.rb
@@ -26,15 +26,14 @@ RSpec.describe Projects::Snippets::BlobsController do
let(:inline) { nil }
subject do
- get(:raw,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- snippet_id: snippet,
- path: filepath,
- ref: ref,
- inline: inline
- })
+ get :raw, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ snippet_id: snippet,
+ path: filepath,
+ ref: ref,
+ inline: inline
+ }
end
context 'with a snippet without a repository' do
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index a388fc4620f..119e52480db 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -102,12 +102,11 @@ RSpec.describe Projects::SnippetsController do
project.add_maintainer(admin)
sign_in(admin)
- post :mark_as_spam,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: snippet.id
- }
+ post :mark_as_spam, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: snippet.id
+ }
end
it 'updates the snippet', :enable_admin_mode do
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 9bc3065b6da..2b3adc719c1 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -21,12 +21,9 @@ RSpec.describe Projects::TreeController do
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- get(:show,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: id
- })
+ get :show, params: {
+ namespace_id: project.namespace.to_param, project_id: project, id: id
+ }
end
context "valid branch, no path" do
@@ -113,12 +110,9 @@ RSpec.describe Projects::TreeController do
allow(::Gitlab::GitalyClient).to receive(:call).and_call_original
expect(::Gitlab::GitalyClient).not_to receive(:call).with(anything, :commit_service, :find_commit, anything, anything)
- get(:show,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: id
- })
+ get :show, params: {
+ namespace_id: project.namespace.to_param, project_id: project, id: id
+ }
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -128,12 +122,9 @@ RSpec.describe Projects::TreeController do
render_views
before do
- get(:show,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: id
- })
+ get :show, params: {
+ namespace_id: project.namespace.to_param, project_id: project, id: id
+ }
end
context 'redirect to blob' do
@@ -141,8 +132,7 @@ RSpec.describe Projects::TreeController do
it 'redirects' do
redirect_url = "/#{project.full_path}/-/blob/master/README.md"
- expect(subject)
- .to redirect_to(redirect_url)
+ expect(subject).to redirect_to(redirect_url)
end
end
end
@@ -151,15 +141,14 @@ RSpec.describe Projects::TreeController do
render_views
before do
- post(:create_dir,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: 'master',
- dir_name: path,
- branch_name: branch_name,
- commit_message: 'Test commit message'
- })
+ post :create_dir, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: 'master',
+ dir_name: path,
+ branch_name: branch_name,
+ commit_message: 'Test commit message'
+ }
end
context 'successful creation' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 51f8a3b1197..cd6d3990309 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -508,11 +508,7 @@ RSpec.describe ProjectsController, feature_category: :projects do
it 'allows an admin user to access the page', :enable_admin_mode do
sign_in(create(:user, :admin))
- get :edit,
- params: {
- namespace_id: project.namespace.path,
- id: project.path
- }
+ get :edit, params: { namespace_id: project.namespace.path, id: project.path }
expect(response).to have_gitlab_http_status(:ok)
end
@@ -521,11 +517,7 @@ RSpec.describe ProjectsController, feature_category: :projects do
sign_in(user)
project.add_maintainer(user)
- get :edit,
- params: {
- namespace_id: project.namespace.path,
- id: project.path
- }
+ get :edit, params: { namespace_id: project.namespace.path, id: project.path }
expect(assigns(:badge_api_endpoint)).not_to be_nil
end
@@ -543,10 +535,7 @@ RSpec.describe ProjectsController, feature_category: :projects do
before do
group.add_owner(user)
- post :archive, params: {
- namespace_id: project.namespace.path,
- id: project.path
- }
+ post :archive, params: { namespace_id: project.namespace.path, id: project.path }
end
it 'archives the project' do
@@ -790,12 +779,7 @@ RSpec.describe ProjectsController, feature_category: :projects do
merge_method: :ff
}
- put :update,
- params: {
- namespace_id: project.namespace,
- id: project.id,
- project: params
- }
+ put :update, params: { namespace_id: project.namespace, id: project.id, project: params }
expect(response).to have_gitlab_http_status(:found)
params.each do |param, value|
@@ -811,22 +795,12 @@ RSpec.describe ProjectsController, feature_category: :projects do
}
expect do
- put :update,
- params: {
- namespace_id: project.namespace,
- id: project.id,
- project: params
- }
+ put :update, params: { namespace_id: project.namespace, id: project.id, project: params }
end.not_to change { project.namespace.reload }
end
def update_project(**parameters)
- put :update,
- params: {
- namespace_id: project.namespace.path,
- id: project.path,
- project: parameters
- }
+ put :update, params: { namespace_id: project.namespace.path, id: project.path, project: parameters }
end
end
@@ -850,12 +824,9 @@ RSpec.describe ProjectsController, feature_category: :projects do
it_behaves_like 'unauthorized when external service denies access' do
subject do
- put :update,
- params: {
- namespace_id: project.namespace,
- id: project,
- project: { description: 'Hello world' }
- }
+ put :update, params: {
+ namespace_id: project.namespace, id: project, project: { description: 'Hello world' }
+ }
project.reload
end
@@ -975,13 +946,9 @@ RSpec.describe ProjectsController, feature_category: :projects do
old_namespace = project.namespace
- put :transfer,
- params: {
- namespace_id: old_namespace.path,
- new_namespace_id: new_namespace_id,
- id: project.path
- },
- format: :js
+ put :transfer, params: {
+ namespace_id: old_namespace.path, new_namespace_id: new_namespace_id, id: project.path
+ }, format: :js
project.reload
@@ -994,13 +961,9 @@ RSpec.describe ProjectsController, feature_category: :projects do
it 'updates namespace' do
sign_in(admin)
- put :transfer,
- params: {
- namespace_id: project.namespace.path,
- new_namespace_id: new_namespace.id,
- id: project.path
- },
- format: :js
+ put :transfer, params: {
+ namespace_id: project.namespace.path, new_namespace_id: new_namespace.id, id: project.path
+ }, format: :js
project.reload
@@ -1120,32 +1083,19 @@ RSpec.describe ProjectsController, feature_category: :projects do
it "toggles star if user is signed in" do
sign_in(user)
expect(user.starred?(public_project)).to be_falsey
- post(:toggle_star,
- params: {
- namespace_id: public_project.namespace,
- id: public_project
- })
+
+ post :toggle_star, params: { namespace_id: public_project.namespace, id: public_project }
expect(user.starred?(public_project)).to be_truthy
- post(:toggle_star,
- params: {
- namespace_id: public_project.namespace,
- id: public_project
- })
+
+ post :toggle_star, params: { namespace_id: public_project.namespace, id: public_project }
expect(user.starred?(public_project)).to be_falsey
end
it "does nothing if user is not signed in" do
- post(:toggle_star,
- params: {
- namespace_id: project.namespace,
- id: public_project
- })
+ post :toggle_star, params: { namespace_id: project.namespace, id: public_project }
expect(user.starred?(public_project)).to be_falsey
- post(:toggle_star,
- params: {
- namespace_id: project.namespace,
- id: public_project
- })
+
+ post :toggle_star, params: { namespace_id: project.namespace, id: public_project }
expect(user.starred?(public_project)).to be_falsey
end
end
@@ -1160,12 +1110,9 @@ RSpec.describe ProjectsController, feature_category: :projects do
let(:forked_project) { fork_project(create(:project, :public), user) }
it 'removes fork from project' do
- delete(:remove_fork,
- params: {
- namespace_id: forked_project.namespace.to_param,
- id: forked_project.to_param
- },
- format: :js)
+ delete :remove_fork, params: {
+ namespace_id: forked_project.namespace.to_param, id: forked_project.to_param
+ }, format: :js
expect(forked_project.reload.forked?).to be_falsey
expect(flash[:notice]).to eq(s_('The fork relationship has been removed.'))
@@ -1177,12 +1124,9 @@ RSpec.describe ProjectsController, feature_category: :projects do
let(:unforked_project) { create(:project, namespace: user.namespace) }
it 'does nothing if project was not forked' do
- delete(:remove_fork,
- params: {
- namespace_id: unforked_project.namespace,
- id: unforked_project
- },
- format: :js)
+ delete :remove_fork, params: {
+ namespace_id: unforked_project.namespace, id: unforked_project
+ }, format: :js
expect(flash[:notice]).to be_nil
expect(response).to redirect_to(edit_project_path(unforked_project))
@@ -1191,12 +1135,10 @@ RSpec.describe ProjectsController, feature_category: :projects do
end
it "does nothing if user is not signed in" do
- delete(:remove_fork,
- params: {
- namespace_id: project.namespace,
- id: project
- },
- format: :js)
+ delete :remove_fork, params: {
+ namespace_id: project.namespace, id: project
+ }, format: :js
+
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
@@ -1289,6 +1231,19 @@ RSpec.describe ProjectsController, feature_category: :projects do
expect(response).to have_gitlab_http_status(:success)
end
end
+
+ context 'when sort param is invalid' do
+ let(:request) { get :refs, params: { namespace_id: project.namespace, id: project, sort: 'invalid' } }
+
+ it 'uses default sort by name' do
+ request
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response['Branches']).to include('master')
+ expect(json_response['Tags']).to include('v1.0.0')
+ expect(json_response['Commits']).to be_nil
+ end
+ end
end
describe 'POST #preview_markdown' do
@@ -1761,12 +1716,7 @@ RSpec.describe ProjectsController, feature_category: :projects do
service_desk_enabled: true
}
- put :update,
- params: {
- namespace_id: project.namespace,
- id: project,
- project: params
- }
+ put :update, params: { namespace_id: project.namespace, id: project, project: params }
project.reload
expect(response).to have_gitlab_http_status(:found)
diff --git a/spec/controllers/registrations/welcome_controller_spec.rb b/spec/controllers/registrations/welcome_controller_spec.rb
index b5416d226e1..3c631362119 100644
--- a/spec/controllers/registrations/welcome_controller_spec.rb
+++ b/spec/controllers/registrations/welcome_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Registrations::WelcomeController, feature_category: :authentication_and_authorization do
+RSpec.describe Registrations::WelcomeController, feature_category: :system_access do
let(:user) { create(:user) }
describe '#welcome' do
@@ -57,6 +57,32 @@ RSpec.describe Registrations::WelcomeController, feature_category: :authenticati
expect(subject).not_to redirect_to(profile_two_factor_auth_path)
end
end
+
+ context 'when welcome step is completed' do
+ before do
+ user.update!(setup_for_company: true)
+ end
+
+ context 'when user is confirmed' do
+ before do
+ sign_in(user)
+ end
+
+ it { is_expected.to redirect_to dashboard_projects_path }
+ end
+
+ context 'when user is not confirmed' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+
+ sign_in(user)
+
+ user.update!(confirmed_at: nil)
+ end
+
+ it { is_expected.to redirect_to user_session_path }
+ end
+ end
end
describe '#update' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index b217b100349..92329b10426 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
end
context 'email confirmation' do
- context 'when `email_confirmation_setting` is set to `hard`' do
+ context 'when email confirmation setting is set to `hard`' do
before do
stub_application_setting_enum('email_confirmation_setting', 'hard')
end
@@ -122,7 +122,7 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
end
context 'email confirmation' do
- context 'when `email_confirmation_setting` is set to `hard`' do
+ context 'when email confirmation setting is set to `hard`' do
before do
stub_application_setting_enum('email_confirmation_setting', 'hard')
stub_feature_flags(identity_verification: false)
@@ -157,7 +157,7 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
stub_feature_flags(identity_verification: false)
end
- context 'when `email_confirmation_setting` is set to `off`' do
+ context 'when email confirmation setting is set to `off`' do
it 'signs the user in' do
stub_application_setting_enum('email_confirmation_setting', 'off')
@@ -166,103 +166,97 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
end
end
- context 'when `email_confirmation_setting` is set to `hard`' do
+ context 'when email confirmation setting is set to `hard`' do
before do
stub_application_setting_enum('email_confirmation_setting', 'hard')
+ allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
end
- context 'when soft email confirmation is not enabled' do
- before do
- stub_feature_flags(soft_email_confirmation: false)
- allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
- end
+ it 'does not authenticate the user and sends a confirmation email' do
+ expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
+ expect(controller.current_user).to be_nil
+ end
- it 'does not authenticate the user and sends a confirmation email' do
- expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
- expect(controller.current_user).to be_nil
- end
+ it 'tracks an almost there redirect' do
+ post_create
- it 'tracks an almost there redirect' do
- post_create
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'render',
+ user: User.find_by(email: base_user_params[:email])
+ )
+ end
- expect_snowplow_event(
- category: described_class.name,
- action: 'render',
- user: User.find_by(email: base_user_params[:email])
- )
- end
+ context 'when registration is triggered from an accepted invite' do
+ context 'when it is part from the initial invite email', :snowplow do
+ let_it_be(:member) { create(:project_member, :invited, invite_email: user_params.dig(:user, :email)) }
- context 'when registration is triggered from an accepted invite' do
- context 'when it is part from the initial invite email', :snowplow do
- let_it_be(:member) { create(:project_member, :invited, invite_email: user_params.dig(:user, :email)) }
+ let(:originating_member_id) { member.id }
+ let(:session_params) do
+ {
+ invite_email: user_params.dig(:user, :email),
+ originating_member_id: originating_member_id
+ }
+ end
- let(:originating_member_id) { member.id }
- let(:session_params) do
- {
- invite_email: user_params.dig(:user, :email),
- originating_member_id: originating_member_id
- }
+ context 'when member exists from the session key value' do
+ it 'tracks the invite acceptance' do
+ subject
+
+ expect_snowplow_event(
+ category: 'RegistrationsController',
+ action: 'accepted',
+ label: 'invite_email',
+ property: member.id.to_s,
+ user: member.reload.user
+ )
+
+ expect_snowplow_event(
+ category: 'RegistrationsController',
+ action: 'create_user',
+ label: 'invited',
+ user: member.reload.user
+ )
end
+ end
- context 'when member exists from the session key value' do
- it 'tracks the invite acceptance' do
- subject
-
- expect_snowplow_event(
- category: 'RegistrationsController',
- action: 'accepted',
- label: 'invite_email',
- property: member.id.to_s,
- user: member.reload.user
- )
-
- expect_snowplow_event(
- category: 'RegistrationsController',
- action: 'create_user',
- label: 'invited',
- user: member.reload.user
- )
- end
- end
+ context 'when member does not exist from the session key value' do
+ let(:originating_member_id) { nil }
+
+ it 'does not track invite acceptance' do
+ subject
- context 'when member does not exist from the session key value' do
- let(:originating_member_id) { nil }
-
- it 'does not track invite acceptance' do
- subject
-
- expect_no_snowplow_event(
- category: 'RegistrationsController',
- action: 'accepted',
- label: 'invite_email'
- )
-
- expect_snowplow_event(
- category: 'RegistrationsController',
- action: 'create_user',
- label: 'signup',
- user: member.reload.user
- )
- end
+ expect_no_snowplow_event(
+ category: 'RegistrationsController',
+ action: 'accepted',
+ label: 'invite_email'
+ )
+
+ expect_snowplow_event(
+ category: 'RegistrationsController',
+ action: 'create_user',
+ label: 'signup',
+ user: member.reload.user
+ )
end
end
+ end
- context 'when invite email matches email used on registration' do
- let(:session_params) { { invite_email: user_params.dig(:user, :email) } }
+ context 'when invite email matches email used on registration' do
+ let(:session_params) { { invite_email: user_params.dig(:user, :email) } }
- it 'signs the user in without sending a confirmation email', :aggregate_failures do
- expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
- expect(controller.current_user).to be_confirmed
- end
+ it 'signs the user in without sending a confirmation email', :aggregate_failures do
+ expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
+ expect(controller.current_user).to be_confirmed
end
+ end
- context 'when invite email does not match the email used on registration' do
- let(:session_params) { { invite_email: 'bogus@email.com' } }
+ context 'when invite email does not match the email used on registration' do
+ let(:session_params) { { invite_email: 'bogus@email.com' } }
- it 'does not authenticate the user and sends a confirmation email', :aggregate_failures do
- expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
- expect(controller.current_user).to be_nil
- end
+ it 'does not authenticate the user and sends a confirmation email', :aggregate_failures do
+ expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
+ expect(controller.current_user).to be_nil
end
end
end
@@ -286,45 +280,45 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
expect(controller.current_user).to be_nil
end
end
+ end
- context 'when soft email confirmation is enabled' do
- before do
- stub_feature_flags(soft_email_confirmation: true)
- allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
- end
+ context 'when email confirmation setting is set to `soft`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
+ allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
+ end
- it 'authenticates the user and sends a confirmation email' do
- expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
- expect(controller.current_user).to be_present
- expect(response).to redirect_to(users_sign_up_welcome_path)
- end
+ it 'authenticates the user and sends a confirmation email' do
+ expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
+ expect(controller.current_user).to be_present
+ expect(response).to redirect_to(users_sign_up_welcome_path)
+ end
- it 'does not track an almost there redirect' do
- post_create
+ it 'does not track an almost there redirect' do
+ post_create
- expect_no_snowplow_event(
- category: described_class.name,
- action: 'render',
- user: User.find_by(email: base_user_params[:email])
- )
- end
+ expect_no_snowplow_event(
+ category: described_class.name,
+ action: 'render',
+ user: User.find_by(email: base_user_params[:email])
+ )
+ end
- context 'when invite email matches email used on registration' do
- let(:session_params) { { invite_email: user_params.dig(:user, :email) } }
+ context 'when invite email matches email used on registration' do
+ let(:session_params) { { invite_email: user_params.dig(:user, :email) } }
- it 'signs the user in without sending a confirmation email', :aggregate_failures do
- expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
- expect(controller.current_user).to be_confirmed
- end
+ it 'signs the user in without sending a confirmation email', :aggregate_failures do
+ expect { subject }.not_to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
+ expect(controller.current_user).to be_confirmed
end
+ end
- context 'when invite email does not match the email used on registration' do
- let(:session_params) { { invite_email: 'bogus@email.com' } }
+ context 'when invite email does not match the email used on registration' do
+ let(:session_params) { { invite_email: 'bogus@email.com' } }
- it 'authenticates the user and sends a confirmation email without confirming', :aggregate_failures do
- expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
- expect(controller.current_user).not_to be_confirmed
- end
+ it 'authenticates the user and sends a confirmation email without confirming', :aggregate_failures do
+ expect { subject }.to have_enqueued_mail(DeviseMailer, :confirmation_instructions)
+ expect(controller.current_user).not_to be_confirmed
end
end
end
@@ -756,8 +750,7 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
m.call(*args)
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'RegistrationsController#destroy')
+ .to include('meta.user' => user.username, 'meta.caller_id' => 'RegistrationsController#destroy')
end
post :destroy
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index da62acb1fda..276bd9b65b9 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Repositories::GitHttpController do
+RSpec.describe Repositories::GitHttpController, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
@@ -14,7 +14,7 @@ RSpec.describe Repositories::GitHttpController do
request.headers.merge! auth_env(user.username, user.password, nil)
end
- context 'when Gitaly is unavailable' do
+ context 'when Gitaly is unavailable', :use_clean_rails_redis_caching do
it 'responds with a 503 message' do
expect(Gitlab::GitalyClient).to receive(:call).and_raise(GRPC::Unavailable)
@@ -26,6 +26,58 @@ RSpec.describe Repositories::GitHttpController do
end
end
+ shared_examples 'handles user activity' do
+ it 'updates the user activity' do
+ activity_project = container.is_a?(PersonalSnippet) ? nil : project
+
+ activity_service = instance_double(Users::ActivityService)
+
+ args = { author: user, project: activity_project, namespace: activity_project&.namespace }
+ expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service)
+
+ expect(activity_service).to receive(:execute)
+
+ get :info_refs, params: params
+ end
+ end
+
+ shared_examples 'handles logging git upload pack operation' do
+ before do
+ password = user.try(:password) || user.try(:token)
+ request.headers.merge! auth_env(user.username, password, nil)
+ end
+
+ context 'with git pull/fetch/clone action' do
+ let(:params) { super().merge(service: 'git-upload-pack') }
+
+ it_behaves_like 'handles user activity'
+ end
+ end
+
+ shared_examples 'handles logging git receive pack operation' do
+ let(:params) { super().merge(service: 'git-receive-pack') }
+
+ before do
+ request.headers.merge! auth_env(user.username, user.password, nil)
+ end
+
+ context 'with git push action when log_user_git_push_activity is enabled' do
+ it_behaves_like 'handles user activity'
+ end
+
+ context 'when log_user_git_push_activity is disabled' do
+ before do
+ stub_feature_flags(log_user_git_push_activity: false)
+ end
+
+ it 'does not log user activity' do
+ expect(controller).not_to receive(:log_user_activity)
+
+ get :info_refs, params: params
+ end
+ end
+ end
+
context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project }
@@ -33,6 +85,8 @@ RSpec.describe Repositories::GitHttpController do
let(:access_checker_class) { Gitlab::GitAccess }
it_behaves_like 'handles unavailable Gitaly'
+ it_behaves_like 'handles logging git upload pack operation'
+ it_behaves_like 'handles logging git receive pack operation'
describe 'POST #git_upload_pack' do
before do
@@ -83,6 +137,8 @@ RSpec.describe Repositories::GitHttpController do
let(:container) { project }
let(:user) { create(:deploy_token, :project, projects: [project]) }
let(:access_checker_class) { Gitlab::GitAccess }
+
+ it_behaves_like 'handles logging git upload pack operation'
end
end
end
@@ -92,6 +148,9 @@ RSpec.describe Repositories::GitHttpController do
let(:container) { create(:project_wiki, :empty_repo, project: project) }
let(:user) { project.first_owner }
let(:access_checker_class) { Gitlab::GitAccessWiki }
+
+ it_behaves_like 'handles logging git upload pack operation'
+ it_behaves_like 'handles logging git receive pack operation'
end
end
@@ -102,6 +161,8 @@ RSpec.describe Repositories::GitHttpController do
let(:access_checker_class) { Gitlab::GitAccessSnippet }
it_behaves_like 'handles unavailable Gitaly'
+ it_behaves_like 'handles logging git upload pack operation'
+ it_behaves_like 'handles logging git receive pack operation'
end
end
@@ -112,6 +173,8 @@ RSpec.describe Repositories::GitHttpController do
let(:access_checker_class) { Gitlab::GitAccessSnippet }
it_behaves_like 'handles unavailable Gitaly'
+ it_behaves_like 'handles logging git upload pack operation'
+ it_behaves_like 'handles logging git receive pack operation'
end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 0f7f4a1910b..ff24b754d7a 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -227,12 +227,10 @@ RSpec.describe SearchController, feature_category: :global_search do
let(:label) { 'redis_hll_counters.search.search_total_unique_counts_monthly' }
let(:property) { 'i_search_total' }
let(:context) do
- [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
- event: property).to_context]
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property).to_context]
end
let(:namespace) { create(:group) }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
context 'on restricted projects' do
@@ -428,6 +426,15 @@ RSpec.describe SearchController, feature_category: :global_search do
get :autocomplete, params: { term: 'setting', filter: 'generic' }
end
+
+ it 'sets correct cache control headers' do
+ get :autocomplete, params: { term: 'setting', filter: 'generic' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(response.headers['Cache-Control']).to eq('max-age=60, private')
+ expect(response.headers['Pragma']).to be_nil
+ end
end
describe '#append_info_to_payload' do
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 1f7d169bae5..80856512bba 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -375,8 +375,7 @@ RSpec.describe SessionsController do
context 'when OTP is valid for another user' do
it 'does not authenticate' do
- authenticate_2fa(login: another_user.username,
- otp_attempt: another_user.current_otp)
+ authenticate_2fa(login: another_user.username, otp_attempt: another_user.current_otp)
expect(subject.current_user).not_to eq another_user
end
@@ -384,8 +383,7 @@ RSpec.describe SessionsController do
context 'when OTP is invalid for another user' do
it 'does not authenticate' do
- authenticate_2fa(login: another_user.username,
- otp_attempt: 'invalid')
+ authenticate_2fa(login: another_user.username, otp_attempt: 'invalid')
expect(subject.current_user).not_to eq another_user
end
@@ -495,51 +493,49 @@ RSpec.describe SessionsController do
end
end
- context 'when using two-factor authentication via U2F device' do
- let(:user) { create(:user, :two_factor) }
+ context 'when using two-factor authentication via WebAuthn device' do
+ let(:user) { create(:user, :two_factor_via_webauthn) }
- def authenticate_2fa_u2f(user_params)
+ def authenticate_2fa(user_params)
post(:create, params: { user: user_params }, session: { otp_user_id: user.id })
end
- before do
- stub_feature_flags(webauthn: false)
- end
-
context 'remember_me field' do
it 'sets a remember_user_token cookie when enabled' do
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
allow(controller).to receive(:find_user).and_return(user)
- expect(controller)
- .to receive(:remember_me).with(user).and_call_original
+ expect(controller).to receive(:remember_me).with(user).and_call_original
- authenticate_2fa_u2f(remember_me: '1', login: user.username, device_response: "{}")
+ authenticate_2fa(remember_me: '1', login: user.username, device_response: "{}")
expect(response.cookies['remember_user_token']).to be_present
end
it 'does nothing when disabled' do
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
allow(controller).to receive(:find_user).and_return(user)
expect(controller).not_to receive(:remember_me)
- authenticate_2fa_u2f(remember_me: '0', login: user.username, device_response: "{}")
+ authenticate_2fa(remember_me: '0', login: user.username, device_response: "{}")
expect(response.cookies['remember_user_token']).to be_nil
end
end
it "creates an audit log record" do
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
- expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { AuditEvent.count }.by(1)
- expect(AuditEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
+ allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
+
+ expect { authenticate_2fa(login: user.username, device_response: "{}") }.to(
+ change { AuditEvent.count }.by(1))
+ expect(AuditEvent.last.details[:with]).to eq("two-factor-via-webauthn-device")
end
it "creates an authentication event record" do
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
- expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { AuthenticationEvent.count }.by(1)
- expect(AuthenticationEvent.last.provider).to eq("two-factor-via-u2f-device")
+ expect { authenticate_2fa(login: user.username, device_response: "{}") }.to(
+ change { AuthenticationEvent.count }.by(1))
+ expect(AuthenticationEvent.last.provider).to eq("two-factor-via-webauthn-device")
end
end
end
@@ -567,8 +563,7 @@ RSpec.describe SessionsController do
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'SessionsController#destroy')
+ .to include('meta.user' => user.username, 'meta.caller_id' => 'SessionsController#destroy')
m.call(*args)
end
@@ -607,8 +602,7 @@ RSpec.describe SessionsController do
m.call(*args)
end
- post(:create,
- params: { user: { login: user.username, password: user.password.succ } })
+ post :create, params: { user: { login: user.username, password: user.password.succ } }
end
end
end
diff --git a/spec/controllers/snippets/blobs_controller_spec.rb b/spec/controllers/snippets/blobs_controller_spec.rb
index b9f58587a58..b92621d4041 100644
--- a/spec/controllers/snippets/blobs_controller_spec.rb
+++ b/spec/controllers/snippets/blobs_controller_spec.rb
@@ -17,13 +17,7 @@ RSpec.describe Snippets::BlobsController do
let(:inline) { nil }
subject do
- get(:raw,
- params: {
- snippet_id: snippet,
- path: filepath,
- ref: ref,
- inline: inline
- })
+ get :raw, params: { snippet_id: snippet, path: filepath, ref: ref, inline: inline }
end
where(:snippet_visibility_level, :user, :status) do
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 6019f10eeeb..b2a62bcfbd6 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -11,9 +11,7 @@ RSpec.describe 'Database schema', feature_category: :database do
IGNORED_INDEXES_ON_FKS = {
slack_integrations_scopes: %w[slack_api_scope_id],
- # Will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/391312
- approval_project_rules: %w[scan_result_policy_id],
- approval_merge_request_rules: %w[scan_result_policy_id]
+ p_ci_builds_metadata: %w[partition_id] # composable FK, the columns are reversed in the index definition
}.with_indifferent_access.freeze
TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
@@ -35,24 +33,24 @@ RSpec.describe 'Database schema', feature_category: :database do
boards: %w[milestone_id iteration_id],
chat_names: %w[chat_id team_id user_id integration_id],
chat_teams: %w[team_id],
- ci_build_needs: %w[partition_id],
+ ci_build_needs: %w[partition_id build_id],
ci_build_pending_states: %w[partition_id build_id],
- ci_build_report_results: %w[partition_id],
+ ci_build_report_results: %w[partition_id build_id],
ci_build_trace_chunks: %w[partition_id build_id],
- ci_build_trace_metadata: %w[partition_id],
+ ci_build_trace_metadata: %w[partition_id build_id],
ci_builds: %w[erased_by_id trigger_request_id partition_id],
ci_builds_runner_session: %w[partition_id build_id],
- p_ci_builds_metadata: %w[partition_id],
- ci_job_artifacts: %w[partition_id],
- ci_job_variables: %w[partition_id],
+ p_ci_builds_metadata: %w[partition_id build_id runner_machine_id],
+ ci_job_artifacts: %w[partition_id job_id],
+ ci_job_variables: %w[partition_id job_id],
ci_namespace_monthly_usages: %w[namespace_id],
- ci_pending_builds: %w[partition_id],
+ ci_pending_builds: %w[partition_id build_id],
ci_pipeline_variables: %w[partition_id],
ci_pipelines: %w[partition_id],
ci_resources: %w[partition_id build_id],
ci_runner_projects: %w[runner_id],
- ci_running_builds: %w[partition_id],
- ci_sources_pipelines: %w[partition_id source_partition_id],
+ ci_running_builds: %w[partition_id build_id],
+ ci_sources_pipelines: %w[partition_id source_partition_id source_job_id],
ci_stages: %w[partition_id],
ci_trigger_requests: %w[commit_id],
ci_unit_test_failures: %w[partition_id build_id],
@@ -91,13 +89,14 @@ RSpec.describe 'Database schema', feature_category: :database do
oauth_access_grants: %w[resource_owner_id application_id],
oauth_access_tokens: %w[resource_owner_id application_id],
oauth_applications: %w[owner_id],
+ p_ci_runner_machine_builds: %w[partition_id build_id],
product_analytics_events_experimental: %w[event_id txn_id user_id],
project_build_artifacts_size_refreshes: %w[last_job_artifact_id],
project_data_transfers: %w[project_id namespace_id],
project_error_tracking_settings: %w[sentry_project_id],
project_group_links: %w[group_id],
project_statistics: %w[namespace_id],
- projects: %w[creator_id ci_id mirror_user_id],
+ projects: %w[ci_id mirror_user_id],
redirect_routes: %w[source_id],
repository_languages: %w[programming_language_id],
routes: %w[source_id],
@@ -171,8 +170,13 @@ RSpec.describe 'Database schema', feature_category: :database do
context 'columns ending with _id' do
let(:column_names) { columns.map(&:name) }
let(:column_names_with_id) { column_names.select { |column_name| column_name.ends_with?('_id') } }
- let(:foreign_keys_columns) { all_foreign_keys.reject { |fk| fk.name&.end_with?("_p") }.map(&:column).uniq } # we can have FK and loose FK present at the same time
let(:ignored_columns) { ignored_fk_columns(table) }
+ let(:foreign_keys_columns) do
+ all_foreign_keys
+ .reject { |fk| fk.name&.end_with?("_p") || fk.name&.end_with?("_id_convert_to_bigint") }
+ .map(&:column)
+ .uniq # we can have FK and loose FK present at the same time
+ end
it 'do have the foreign keys' do
expect(column_names_with_id - ignored_columns).to match_array(foreign_keys_columns)
diff --git a/spec/deprecation_warnings.rb b/spec/deprecation_warnings.rb
new file mode 100644
index 00000000000..45fed5fecca
--- /dev/null
+++ b/spec/deprecation_warnings.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative '../lib/gitlab/utils'
+return if Gitlab::Utils.to_boolean(ENV['SILENCE_DEPRECATIONS'], default: false)
+
+# Enable deprecation warnings by default and make them more visible
+# to developers to ease upgrading to newer Ruby versions.
+Warning[:deprecated] = true
+
+# rubocop:disable Layout/LineLength
+case RUBY_VERSION[/\d+\.\d+/, 0]
+when '3.2'
+ warn "#{__FILE__}:#{__LINE__}: warning: Ignored warnings for Ruby < 3.2 are no longer necessary."
+else
+ require 'warning'
+ # Ignore Ruby warnings until Ruby 3.2.
+ # ... ruby/3.1.3/lib/ruby/gems/3.1.0/gems/rspec-parameterized-table_syntax-1.0.0/lib/rspec/parameterized/table_syntax.rb:38: warning: Refinement#include is deprecated and will be removed in Ruby 3.2
+
+ Warning.ignore(%r{rspec-parameterized-table_syntax-1\.0\.0/lib/rspec/parameterized/table_syntax\.rb:\d+: warning: Refinement#include is deprecated})
+end
+# rubocop:enable Layout/LineLength
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index 7aca5e492f4..ef8f8cbce3b 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ApplicationExperiment, :experiment do
+RSpec.describe ApplicationExperiment, :experiment, feature_category: :experimentation_conversion do
subject(:application_experiment) { described_class.new('namespaced/stub', **context) }
let(:context) { {} }
@@ -187,13 +187,11 @@ RSpec.describe ApplicationExperiment, :experiment do
end
with_them do
- it "returns the url or nil if invalid" do
- allow(Gitlab).to receive(:com?).and_return(true)
+ it "returns the url or nil if invalid on SaaS", :saas do
expect(application_experiment.process_redirect_url(url)).to eq(processed_url)
end
- it "considers all urls invalid when not on dev or com" do
- allow(Gitlab).to receive(:com?).and_return(false)
+ it "considers all urls invalid when not on SaaS" do
expect(application_experiment.process_redirect_url(url)).to be_nil
end
end
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 355fb142994..9f05d183ba4 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -7,5 +7,9 @@ FactoryBot.define do
message { 'User sends spam' }
reported_from_url { 'http://gitlab.com' }
links_to_spam { ['https://gitlab.com/issue1', 'https://gitlab.com/issue2'] }
+
+ trait :closed do
+ status { 'closed' }
+ end
end
end
diff --git a/spec/factories/achievements/user_achievements.rb b/spec/factories/achievements/user_achievements.rb
new file mode 100644
index 00000000000..a5fd1df38dd
--- /dev/null
+++ b/spec/factories/achievements/user_achievements.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user_achievement, class: 'Achievements::UserAchievement' do
+ user
+ achievement
+ awarded_by_user factory: :user
+
+ trait :revoked do
+ revoked_by_user factory: :user
+ revoked_at { Time.now }
+ end
+ end
+end
diff --git a/spec/factories/airflow/dags.rb b/spec/factories/airflow/dags.rb
deleted file mode 100644
index ca4276e2c8f..00000000000
--- a/spec/factories/airflow/dags.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-FactoryBot.define do
- factory :airflow_dags, class: '::Airflow::Dags' do
- sequence(:dag_name) { |n| "dag_name_#{n}" }
-
- project
- end
-end
diff --git a/spec/factories/bulk_import/batch_trackers.rb b/spec/factories/bulk_import/batch_trackers.rb
new file mode 100644
index 00000000000..427eefc5f3e
--- /dev/null
+++ b/spec/factories/bulk_import/batch_trackers.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :bulk_import_batch_tracker, class: 'BulkImports::BatchTracker' do
+ association :tracker, factory: :bulk_import_tracker
+
+ status { 0 }
+ fetched_objects_count { 1000 }
+ imported_objects_count { 1000 }
+
+ sequence(:batch_number) { |n| n }
+
+ trait :created do
+ status { 0 }
+ end
+
+ trait :started do
+ status { 1 }
+ end
+
+ trait :finished do
+ status { 2 }
+ end
+
+ trait :timeout do
+ status { 3 }
+ end
+
+ trait :failed do
+ status { -1 }
+ end
+
+ trait :skipped do
+ status { -2 }
+ end
+ end
+end
diff --git a/spec/factories/bulk_import/export_batches.rb b/spec/factories/bulk_import/export_batches.rb
new file mode 100644
index 00000000000..4339b02d27e
--- /dev/null
+++ b/spec/factories/bulk_import/export_batches.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :bulk_import_export_batch, class: 'BulkImports::ExportBatch' do
+ association :export, factory: :bulk_import_export
+
+ upload { association(:bulk_import_export_upload) }
+
+ status { 0 }
+
+ trait :started do
+ status { 0 }
+ end
+
+ trait :finished do
+ status { 1 }
+ end
+
+ trait :failed do
+ status { -1 }
+ end
+ end
+end
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
index 56567394bf5..c872694ee64 100644
--- a/spec/factories/chat_names.rb
+++ b/spec/factories/chat_names.rb
@@ -3,7 +3,6 @@
FactoryBot.define do
factory :chat_name, class: 'ChatName' do
user
- integration
team_id { 'T0001' }
team_domain { 'Awesome Team' }
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 224f460488b..dc75e17499c 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -415,7 +415,7 @@ FactoryBot.define do
runner factory: :ci_runner
after(:create) do |build|
- build.create_runtime_metadata!
+ ::Ci::RunningBuild.upsert_shared_runner_build!(build)
end
end
@@ -694,7 +694,7 @@ FactoryBot.define do
end
end
- trait :non_public_artifacts do
+ trait :with_private_artifacts_config do
options do
{
artifacts: { public: false }
@@ -702,6 +702,14 @@ FactoryBot.define do
end
end
+ trait :with_public_artifacts_config do
+ options do
+ {
+ artifacts: { public: true }
+ }
+ end
+ end
+
trait :non_playable do
status { 'created' }
self.when { 'manual' }
diff --git a/spec/factories/ci/catalog/resources.rb b/spec/factories/ci/catalog/resources.rb
new file mode 100644
index 00000000000..66c2e58cdd9
--- /dev/null
+++ b/spec/factories/ci/catalog/resources.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :catalog_resource, class: 'Ci::Catalog::Resource' do
+ project factory: :project
+ end
+end
diff --git a/spec/factories/ci/runner_machine_builds.rb b/spec/factories/ci/runner_machine_builds.rb
new file mode 100644
index 00000000000..0181def26ba
--- /dev/null
+++ b/spec/factories/ci/runner_machine_builds.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_runner_machine_build, class: 'Ci::RunnerMachineBuild' do
+ build factory: :ci_build, scheduling_type: :dag
+ runner_machine factory: :ci_runner_machine
+ end
+end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 4758986b47c..a9a637b4284 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -66,6 +66,12 @@ FactoryBot.define do
end
end
+ trait :with_runner_machine do
+ after(:build) do |runner, evaluator|
+ runner.runner_machines << build(:ci_runner_machine, runner: runner)
+ end
+ end
+
trait :inactive do
active { false }
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 0647058d63a..99110a9b841 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -98,15 +98,6 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
- factory :clusters_applications_crossplane, class: 'Clusters::Applications::Crossplane' do
- stack { 'gcp' }
- cluster factory: %i(cluster with_installed_helm provided_by_gcp)
- end
-
- factory :clusters_applications_prometheus, class: 'Clusters::Applications::Prometheus' do
- cluster factory: %i(cluster with_installed_helm provided_by_gcp)
- end
-
factory :clusters_applications_runner, class: 'Clusters::Applications::Runner' do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 32cd6beb7ea..d92ee6dcbe7 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -87,15 +87,12 @@ FactoryBot.define do
end
trait :with_installed_prometheus do
- application_prometheus factory: %i(clusters_applications_prometheus installed)
integration_prometheus factory: %i(clusters_integrations_prometheus)
end
trait :with_all_applications do
application_helm factory: %i(clusters_applications_helm installed)
application_ingress factory: %i(clusters_applications_ingress installed)
- application_crossplane factory: %i(clusters_applications_crossplane installed)
- application_prometheus factory: %i(clusters_applications_prometheus installed)
application_runner factory: %i(clusters_applications_runner installed)
application_jupyter factory: %i(clusters_applications_jupyter installed)
application_knative factory: %i(clusters_applications_knative installed)
diff --git a/spec/factories/gitlab/database/async_foreign_keys/postgres_async_constraint_validation.rb b/spec/factories/gitlab/database/async_foreign_keys/postgres_async_constraint_validation.rb
new file mode 100644
index 00000000000..81f67e958c0
--- /dev/null
+++ b/spec/factories/gitlab/database/async_foreign_keys/postgres_async_constraint_validation.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :postgres_async_constraint_validation,
+ class: 'Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation' do
+ sequence(:name) { |n| "fk_users_id_#{n}" }
+ table_name { "users" }
+
+ trait :foreign_key do
+ constraint_type { :foreign_key }
+ end
+
+ trait :check_constraint do
+ constraint_type { :check_constraint }
+ end
+ end
+end
diff --git a/spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb b/spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb
deleted file mode 100644
index a61b5cde7a0..00000000000
--- a/spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :postgres_async_foreign_key_validation,
- class: 'Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation' do
- sequence(:name) { |n| "fk_users_id_#{n}" }
- table_name { "users" }
- end
-end
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 7740b2da911..caeac6e3b92 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -261,7 +261,26 @@ FactoryBot.define do
app_store_issuer_id { 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' }
app_store_key_id { 'ABC1' }
- app_store_private_key { File.read('spec/fixtures/ssl_key.pem') }
+ app_store_private_key_file_name { 'auth_key.p8' }
+ app_store_private_key { File.read('spec/fixtures/auth_key.p8') }
+ end
+
+ factory :google_play_integration, class: 'Integrations::GooglePlay' do
+ project
+ active { true }
+ type { 'Integrations::GooglePlay' }
+
+ service_account_key_file_name { 'service_account.json' }
+ service_account_key { File.read('spec/fixtures/service_account.json') }
+ end
+
+ factory :squash_tm_integration, class: 'Integrations::SquashTm' do
+ project
+ active { true }
+ type { 'Integrations::SquashTm' }
+
+ url { 'https://url-to-squash.com' }
+ token { 'squash_tm_token' }
end
# this is for testing storing values inside properties, which is deprecated and will be removed in
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 530b4616765..2a21bde5436 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -12,6 +12,7 @@ FactoryBot.define do
factory :note_on_commit, traits: [:on_commit]
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
+ factory :note_on_work_item, traits: [:on_work_item]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
@@ -122,6 +123,10 @@ FactoryBot.define do
noteable { association(:issue, project: project) }
end
+ trait :on_work_item do
+ noteable { association(:work_item, project: project) }
+ end
+
trait :on_merge_request do
noteable { association(:merge_request, source_project: project) }
end
diff --git a/spec/factories/packages/debian/component_file.rb b/spec/factories/packages/debian/component_file.rb
index a2422e4a126..eefc6ab5966 100644
--- a/spec/factories/packages/debian/component_file.rb
+++ b/spec/factories/packages/debian/component_file.rb
@@ -47,5 +47,12 @@ FactoryBot.define do
trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end
+
+ trait(:empty) do
+ file_md5 { 'd41d8cd98f00b204e9800998ecf8427e' }
+ file_sha256 { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' }
+ file_fixture { nil }
+ size { 0 }
+ end
end
end
diff --git a/spec/factories/packages/debian/file_metadatum.rb b/spec/factories/packages/debian/file_metadatum.rb
index 505b9975f79..ef6c4e1f222 100644
--- a/spec/factories/packages/debian/file_metadatum.rb
+++ b/spec/factories/packages/debian/file_metadatum.rb
@@ -30,7 +30,7 @@ FactoryBot.define do
{
'Format' => '3.0 (native)',
'Source' => package_file.package.name,
- 'Binary' => 'sample-dev, libsample0, sample-udeb',
+ 'Binary' => 'sample-dev, libsample0, sample-udeb, sample-ddeb',
'Architecture' => 'any',
'Version': package_file.package.version,
'Maintainer' => "#{FFaker::Name.name} <#{FFaker::Internet.email}>",
@@ -39,12 +39,13 @@ FactoryBot.define do
'Build-Depends' => 'debhelper-compat (= 13)',
'Package-List' => <<~EOF.rstrip,
libsample0 deb libs optional arch=any',
+ 'sample-ddeb deb libs optional arch=any',
sample-dev deb libdevel optional arch=any',
sample-udeb udeb libs optional arch=any',
EOF
- 'Checksums-Sha1' => "\nc5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz",
- 'Checksums-Sha256' => "\n40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz",
- 'Files' => "\nd5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz"
+ 'Checksums-Sha1' => "\n4a9cb2a7c77a68dc0fe54ba8ecef133a7c949e9d 964 sample_1.2.3~alpha2.tar.xz",
+ 'Checksums-Sha256' => "\nc9d05185ca158bb804977fa9d7b922e8a0f644a2da41f99d2787dd61b1e2e2c5 964 sample_1.2.3~alpha2.tar.xz",
+ 'Files' => "\nadc69e57cda38d9bb7c8d59cacfb6869 964 sample_1.2.3~alpha2.tar.xz"
}
end
end
@@ -109,6 +110,13 @@ FactoryBot.define do
fields { { 'a': 'b' } }
end
+ trait(:ddeb) do
+ file_type { 'ddeb' }
+ component { 'main' }
+ architecture { 'amd64' }
+ fields { { 'a': 'b' } }
+ end
+
trait(:buildinfo) do
file_type { 'buildinfo' }
component { 'main' }
diff --git a/spec/factories/packages/package_files.rb b/spec/factories/packages/package_files.rb
index 7d3dd274777..ababa8fa7f5 100644
--- a/spec/factories/packages/package_files.rb
+++ b/spec/factories/packages/package_files.rb
@@ -131,9 +131,9 @@ FactoryBot.define do
trait(:source) do
file_name { 'sample_1.2.3~alpha2.tar.xz' }
- file_md5 { 'd5ca476e4229d135a88f9c729c7606c9' }
- file_sha1 { 'c5cfc111ea924842a89a06d5673f07dfd07de8ca' }
- file_sha256 { '40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da' }
+ file_md5 { 'adc69e57cda38d9bb7c8d59cacfb6869' }
+ file_sha1 { '4a9cb2a7c77a68dc0fe54ba8ecef133a7c949e9d' }
+ file_sha256 { 'c9d05185ca158bb804977fa9d7b922e8a0f644a2da41f99d2787dd61b1e2e2c5' }
transient do
file_metadatum_trait { :source }
@@ -142,9 +142,9 @@ FactoryBot.define do
trait(:dsc) do
file_name { 'sample_1.2.3~alpha2.dsc' }
- file_md5 { 'ceccb6bb3e45ce6550b24234d4023e0f' }
- file_sha1 { '375ba20ea1789e1e90d469c3454ce49a431d0442' }
- file_sha256 { '81fc156ba937cdb6215362cc4bf6b8dc47be9b4253ba0f1a4ab10c7ea0c4c4e5' }
+ file_md5 { '629921cfc477bfa84adfd2ccaba89783' }
+ file_sha1 { '443c98a4cf4acd21e2259ae8f2d60fc9932de353' }
+ file_sha256 { 'f91070524a59bbb3a1f05a78409e92cb9ee86470b34018bc0b93bd5b2dd3868c' }
transient do
file_metadatum_trait { :dsc }
@@ -184,11 +184,22 @@ FactoryBot.define do
end
end
+ trait(:ddeb) do
+ file_name { 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' }
+ file_md5 { '90d1107471eed48c73ad78b19ac83639' }
+ file_sha1 { '9c5af97cf8dfbe8126c807f540c88757f382b307' }
+ file_sha256 { 'a6bcc8a4b010f99ce0ea566ac69088e1910e754593c77f2b4942e3473e784e4d' }
+
+ transient do
+ file_metadatum_trait { :ddeb }
+ end
+ end
+
trait(:buildinfo) do
file_name { 'sample_1.2.3~alpha2_amd64.buildinfo' }
- file_md5 { '12a5ac4f16ad75f8741327ac23b4c0d7' }
- file_sha1 { '661f7507efa6fdd3763c95581d0baadb978b7ef5' }
- file_sha256 { 'd0c169e9caa5b303a914b27b5adf69768fe6687d4925905b7d0cd9c0f9d4e56c' }
+ file_md5 { 'cc07ff4d741aec132816f9bd67c6875d' }
+ file_sha1 { 'bcc4ca85f17a31066b726cd4e04485ab24a682c6' }
+ file_sha256 { '5a3dac17c4ff0d49fa5f47baa973902b59ad2ee05147062b8ed8f19d196731d1' }
transient do
file_metadatum_trait { :buildinfo }
diff --git a/spec/factories/packages/packages.rb b/spec/factories/packages/packages.rb
index d0fde0a16cd..1d5119638ca 100644
--- a/spec/factories/packages/packages.rb
+++ b/spec/factories/packages/packages.rb
@@ -91,6 +91,7 @@ FactoryBot.define do
create :debian_package_file, :deb, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :deb_dev, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :udeb, evaluator.file_metadatum_trait, package: package
+ create :debian_package_file, :ddeb, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :buildinfo, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :changes, evaluator.file_metadatum_trait, package: package
end
diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb
index a8ad1af6345..dc0277cb58d 100644
--- a/spec/factories/project_error_tracking_settings.rb
+++ b/spec/factories/project_error_tracking_settings.rb
@@ -16,7 +16,12 @@ FactoryBot.define do
end
trait :integrated do
+ api_url { nil }
integrated { true }
+ token { nil }
+ project_name { nil }
+ organization_name { nil }
+ sentry_project_id { nil }
end
end
end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 946b3925ee9..a7f562df92d 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -35,7 +35,7 @@ FactoryBot.define do
end
trait :permanently_disabled do
- recent_failures { WebHook::FAILURE_THRESHOLD + 1 }
+ recent_failures { WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1 }
end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f113ca2425f..299dd165807 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -222,7 +222,7 @@ FactoryBot.define do
# the transient `files` attribute. Each file will be created in its own
# commit, operating against the master branch. So, the following call:
#
- # create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo', 'b.txt' => bar' })
+ # create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo', 'b.txt' => 'bar' })
#
# will create a repository containing two files, and two commits, in master
trait :custom_repo do
@@ -245,6 +245,19 @@ FactoryBot.define do
end
end
+ # A basic repository with a single file 'test.txt'. It also has the HEAD as the default branch.
+ trait :small_repo do
+ custom_repo
+
+ files { { 'test.txt' => 'test' } }
+
+ after(:create) do |project|
+ Sidekiq::Worker.skipping_transaction_check do
+ raise "Failed to assign the repository head!" unless project.change_head(project.default_branch_or_main)
+ end
+ end
+ end
+
# Test repository - https://gitlab.com/gitlab-org/gitlab-test
trait :repository do
test_repo
@@ -354,6 +367,18 @@ FactoryBot.define do
end
end
+ trait :stubbed_commit_count do
+ after(:build) do |project|
+ stub_method(project.repository, :commit_count) { 2 }
+ end
+ end
+
+ trait :stubbed_branch_count do
+ after(:build) do |project|
+ stub_method(project.repository, :branch_count) { 2 }
+ end
+ end
+
trait :wiki_repo do
after(:create) do |project|
stub_feature_flags(main_branch_over_master: false)
diff --git a/spec/factories/serverless/domain.rb b/spec/factories/serverless/domain.rb
deleted file mode 100644
index c09af068d19..00000000000
--- a/spec/factories/serverless/domain.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :serverless_domain, class: '::Serverless::Domain' do
- function_name { 'test-function' }
- serverless_domain_cluster { association(:serverless_domain_cluster) }
- environment { association(:environment) }
-
- skip_create
- end
-end
diff --git a/spec/factories/serverless/domain_cluster.rb b/spec/factories/serverless/domain_cluster.rb
deleted file mode 100644
index e8ff6cf42b2..00000000000
--- a/spec/factories/serverless/domain_cluster.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :serverless_domain_cluster, class: '::Serverless::DomainCluster' do
- pages_domain { association(:pages_domain) }
- knative { association(:clusters_applications_knative) }
- creator { association(:user) }
-
- certificate do
- File.read(Rails.root.join('spec/fixtures/', 'ssl_certificate.pem'))
- end
-
- key do
- File.read(Rails.root.join('spec/fixtures/', 'ssl_key.pem'))
- end
- end
-end
diff --git a/spec/factories/service_desk/custom_email_verification.rb b/spec/factories/service_desk/custom_email_verification.rb
new file mode 100644
index 00000000000..614be5da71e
--- /dev/null
+++ b/spec/factories/service_desk/custom_email_verification.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :service_desk_custom_email_verification, class: '::ServiceDesk::CustomEmailVerification' do
+ project
+ state { "running" }
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e641f925758..10de7bc3b5b 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -10,6 +10,7 @@ FactoryBot.define do
confirmed_at { Time.now }
confirmation_token { nil }
can_create_group { true }
+ color_scheme_id { 1 }
trait :admin do
admin { true }
@@ -59,6 +60,10 @@ FactoryBot.define do
user_type { :project_bot }
end
+ trait :service_account do
+ user_type { :service_account }
+ end
+
trait :migration_bot do
user_type { :migration_bot }
end
diff --git a/spec/factories/users/banned_users.rb b/spec/factories/users/banned_users.rb
new file mode 100644
index 00000000000..f2b6eb5893a
--- /dev/null
+++ b/spec/factories/users/banned_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :banned_user, class: 'Users::BannedUser' do
+ user { association(:user) }
+ end
+end
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index cff246d4071..3cb4d8cd8bc 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -37,5 +37,12 @@ FactoryBot.define do
issue_type { :key_result }
association :work_item_type, :default, :key_result
end
+
+ before(:create, :build) do |work_item, evaluator|
+ if evaluator.namespace.present?
+ work_item.project = nil
+ work_item.namespace = evaluator.namespace
+ end
+ end
end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 0df771b4025..fcf0c43243f 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -11,34 +11,24 @@ require_relative '../config/bundler_setup'
ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
+require './spec/deprecation_warnings'
+
# 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 'active_support/all'
+require 'pry'
+require_relative 'rails_autoload'
+
require_relative '../config/settings'
require_relative 'support/rspec'
require_relative '../lib/gitlab/utils'
require_relative '../lib/gitlab/utils/strong_memoize'
-require 'active_support/all'
-require 'pry'
require_relative 'simplecov_env'
SimpleCovEnv.start!
-unless ActiveSupport::Dependencies.autoload_paths.frozen?
- ActiveSupport::Dependencies.autoload_paths << 'lib'
- ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
- ActiveSupport::Dependencies.autoload_paths << 'jh/lib'
-end
-
ActiveSupport::XmlMini.backend = 'Nokogiri'
-RSpec.configure do |config|
- # Makes diffs show entire non-truncated values.
- config.before(:each, unlimited_max_formatted_output_length: true) do |_example|
- config.expect_with :rspec do |c|
- c.max_formatted_output_length = nil
- end
- end
-end
+# Consider tweaking configuration in `spec/support/rspec.rb` which is also
+# used by `spec/spec_helper.rb`.
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index 1267025a7bf..474ab4c7b8e 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -15,25 +15,6 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
end
describe 'report abuse to administrator' do
- shared_examples 'reports the user with an abuse category' do
- it do
- fill_and_submit_abuse_category_form
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
- end
- end
-
- shared_examples 'reports the user without an abuse category' do
- it do
- click_link 'Report abuse to administrator'
-
- fill_and_submit_report_abuse_form
-
- expect(page).to have_content 'Thank you for your report'
- end
- end
-
context 'when reporting an issue for abuse' do
before do
visit project_issue_path(project, issue)
diff --git a/spec/features/action_cable_logging_spec.rb b/spec/features/action_cable_logging_spec.rb
index c02a41c4c59..c8a4e1efb7a 100644
--- a/spec/features/action_cable_logging_spec.rb
+++ b/spec/features/action_cable_logging_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'ActionCable logging', :js, feature_category: :not_owned do
+RSpec.describe 'ActionCable logging', :js, feature_category: :shared do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 10f12d7116f..9fe72b981f1 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -2,79 +2,210 @@
require 'spec_helper'
-RSpec.describe "Admin::AbuseReports", :js, feature_category: :not_owned do
- let(:user) { create(:user) }
+RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
context 'as an admin' do
- before do
- admin = create(:admin)
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
- end
+ describe 'displayed reports' do
+ include FilteredSearchHelpers
- describe 'if a user has been reported for abuse' do
- let!(:abuse_report) { create(:abuse_report, user: user) }
+ let_it_be(:open_report) { create(:abuse_report, created_at: 5.days.ago, updated_at: 2.days.ago) }
+ let_it_be(:open_report2) { create(:abuse_report, created_at: 4.days.ago, updated_at: 3.days.ago, category: 'phishing') }
+ let_it_be(:closed_report) { create(:abuse_report, :closed) }
- describe 'in the abuse report view' do
- it 'presents information about abuse report' do
- visit admin_abuse_reports_path
+ let(:abuse_report_row_selector) { '[data-testid="abuse-report-row"]' }
- expect(page).to have_content('Abuse Reports')
- expect(page).to have_content(abuse_report.message)
- expect(page).to have_link(user.name, href: user_path(user))
- expect(page).to have_link('Remove user')
- end
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+
+ visit admin_abuse_reports_path
end
- describe 'in the profile page of the user' do
- it 'shows a link to the admin view of the user' do
- visit user_path(user)
+ it 'only includes open reports by default' do
+ expect_displayed_reports_count(2)
- expect(page).to have_link '', href: admin_user_path(user)
+ expect_report_shown(open_report, open_report2)
+
+ within '[data-testid="abuse-reports-filtered-search-bar"]' do
+ expect(page).to have_content 'Status = Open'
end
end
- end
- describe 'if a many users have been reported for abuse' do
- let(:report_count) { AbuseReport.default_per_page + 3 }
+ it 'can be filtered by status, user, reporter, and category', :aggregate_failures do
+ # filter by status
+ filter %w[Status Closed]
+ expect_displayed_reports_count(1)
+ expect_report_shown(closed_report)
+ expect_report_not_shown(open_report, open_report2)
- before do
- report_count.times do
- create(:abuse_report, user: create(:user))
+ filter %w[Status Open]
+ expect_displayed_reports_count(2)
+ expect_report_shown(open_report, open_report2)
+ expect_report_not_shown(closed_report)
+
+ # filter by user
+ filter(['User', open_report2.user.username])
+
+ expect_displayed_reports_count(1)
+ expect_report_shown(open_report2)
+ expect_report_not_shown(open_report, closed_report)
+
+ # filter by reporter
+ filter(['Reporter', open_report.reporter.username])
+
+ expect_displayed_reports_count(1)
+ expect_report_shown(open_report)
+ expect_report_not_shown(open_report2, closed_report)
+
+ # filter by category
+ filter(['Category', open_report2.category])
+
+ expect_displayed_reports_count(1)
+ expect_report_shown(open_report2)
+ expect_report_not_shown(open_report, closed_report)
+ end
+
+ it 'can be sorted by created_at and updated_at in desc and asc order', :aggregate_failures do
+ # created_at desc (default)
+ expect(report_rows[0].text).to include(report_text(open_report2))
+ expect(report_rows[1].text).to include(report_text(open_report))
+
+ # created_at asc
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(report_text(open_report))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+
+ # updated_at ascending
+ sort_by 'Updated date'
+
+ expect(report_rows[0].text).to include(report_text(open_report2))
+ expect(report_rows[1].text).to include(report_text(open_report))
+
+ # updated_at descending
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(report_text(open_report))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+ end
+
+ def report_rows
+ page.all(abuse_report_row_selector)
+ end
+
+ def report_text(report)
+ "#{report.user.name} reported for #{report.category}"
+ end
+
+ def expect_report_shown(*reports)
+ reports.each do |r|
+ expect(page).to have_content(report_text(r))
end
end
- describe 'in the abuse report view' do
- it 'presents information about abuse report' do
- visit admin_abuse_reports_path
+ def expect_report_not_shown(*reports)
+ reports.each do |r|
+ expect(page).not_to have_content(report_text(r))
+ end
+ end
- expect(page).to have_selector('.pagination')
- expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ def expect_displayed_reports_count(count)
+ expect(page).to have_css(abuse_report_row_selector, count: count)
+ end
+
+ def filter(tokens)
+ # remove all existing filters first
+ page.find_all('.gl-token-close').each(&:click)
+
+ select_tokens(*tokens, submit: true, input_text: 'Filter reports')
+ end
+
+ def sort_by(sort)
+ page.within('.vue-filtered-search-bar-container .sort-dropdown-container') do
+ page.find('.gl-dropdown-toggle').click
+
+ page.within('.dropdown-menu') do
+ click_button sort
+ wait_for_requests
+ end
end
end
end
- describe 'filtering by user' do
- let!(:user2) { create(:user) }
- let!(:abuse_report) { create(:abuse_report, user: user) }
- let!(:abuse_report_2) { create(:abuse_report, user: user2) }
+ context 'when abuse_reports_list feature flag is disabled' do
+ before do
+ stub_feature_flags(abuse_reports_list: false)
+
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
+
+ describe 'if a user has been reported for abuse' do
+ let!(:abuse_report) { create(:abuse_report, user: user) }
- it 'shows only single user report' do
- visit admin_abuse_reports_path
+ describe 'in the abuse report view' do
+ it 'presents information about abuse report' do
+ visit admin_abuse_reports_path
+
+ expect(page).to have_content('Abuse Reports')
+ expect(page).to have_content(abuse_report.message)
+ expect(page).to have_link(user.name, href: user_path(user))
+ expect(page).to have_link('Remove user')
+ end
+ end
+
+ describe 'in the profile page of the user' do
+ it 'shows a link to the admin view of the user' do
+ visit user_path(user)
- page.within '.filter-form' do
- click_button 'User'
- wait_for_requests
+ expect(page).to have_link '', href: admin_user_path(user)
+ end
+ end
+ end
+
+ describe 'if a many users have been reported for abuse' do
+ let(:report_count) { AbuseReport.default_per_page + 3 }
- page.within '.dropdown-menu-user' do
- click_link user2.name
+ before do
+ report_count.times do
+ create(:abuse_report, user: create(:user))
end
+ end
+
+ describe 'in the abuse report view' do
+ it 'presents information about abuse report' do
+ visit admin_abuse_reports_path
- wait_for_requests
+ expect(page).to have_selector('.pagination')
+ expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ end
end
+ end
- expect(page).to have_content(user2.name)
- expect(page).not_to have_content(user.name)
+ describe 'filtering by user' do
+ let!(:user2) { create(:user) }
+ let!(:abuse_report) { create(:abuse_report, user: user) }
+ let!(:abuse_report_2) { create(:abuse_report, user: user2) }
+
+ it 'shows only single user report' do
+ visit admin_abuse_reports_path
+
+ page.within '.filter-form' do
+ click_button 'User'
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ wait_for_requests
+ end
+
+ expect(page).to have_content(user2.name)
+ expect(page).not_to have_content(user.name)
+ end
end
end
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 252d9ac5bac..fee75496a1e 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Appearance', feature_category: :not_owned do
+RSpec.describe 'Admin Appearance', feature_category: :shared do
let!(:appearance) { create(:appearance) }
let(:admin) { create(:admin) }
diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
index 461c9d08273..c272a8630b7 100644
--- a/spec/features/admin/admin_browse_spam_logs_spec.rb
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin browse spam logs', feature_category: :not_owned do
+RSpec.describe 'Admin browse spam logs', feature_category: :shared do
let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) }
before do
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
index e55e1cce6b9..f59b4db5cc2 100644
--- a/spec/features/admin/admin_deploy_keys_spec.rb
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin deploy keys', :js, feature_category: :authentication_and_authorization do
+RSpec.describe 'admin deploy keys', :js, feature_category: :system_access do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index de71a48d2dc..23a9ab74a7a 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe "Admin Health Check", feature_category: :continuous_verification do
+RSpec.describe "Admin Health Check", :js, feature_category: :continuous_verification do
include StubENV
+ include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
before do
@@ -30,7 +31,8 @@ RSpec.describe "Admin Health Check", feature_category: :continuous_verification
describe 'reload access token' do
it 'changes the access token' do
orig_token = Gitlab::CurrentSettings.health_check_access_token
- click_button 'Reset health check access token'
+ click_link 'Reset health check access token'
+ accept_gl_confirm('Are you sure you want to reset the health check token?')
expect(page).to have_content('New health check access token has been generated!')
expect(find('#health-check-token').text).not_to eq orig_token
diff --git a/spec/features/admin/admin_mode/login_spec.rb b/spec/features/admin/admin_mode/login_spec.rb
index 393721fe451..853e4763872 100644
--- a/spec/features/admin/admin_mode/login_spec.rb
+++ b/spec/features/admin/admin_mode/login_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authorization do
+RSpec.describe 'Admin Mode Login', feature_category: :system_access do
include TermsHelper
include UserLoginHelper
include LdapHelpers
@@ -25,13 +25,13 @@ RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authori
it 'blocks login if we reuse the same code immediately' do
gitlab_sign_in(user, remember: true)
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
repeated_otp = user.current_otp
enter_code(repeated_otp)
gitlab_enable_admin_mode_sign_in(user)
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
enter_code(repeated_otp)
@@ -43,12 +43,12 @@ RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authori
before do
gitlab_sign_in(user, remember: true)
- expect(page).to have_content('Two-factor authentication code')
+ expect(page).to have_content('Enter verification code')
enter_code(user.current_otp)
gitlab_enable_admin_mode_sign_in(user)
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
end
it 'allows login with valid code' do
@@ -145,11 +145,11 @@ RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authori
it 'signs user in without prompting for second factor' do
sign_in_using_saml!
- expect(page).not_to have_content('Two-Factor Authentication')
+ expect(page).not_to have_content(_('Enter verification code'))
enable_admin_mode_using_saml!
- expect(page).not_to have_content('Two-Factor Authentication')
+ expect(page).not_to have_content(_('Enter verification code'))
expect(page).to have_current_path admin_root_path, ignore_query: true
expect(page).to have_content('Admin mode enabled')
end
@@ -159,12 +159,12 @@ RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authori
it 'shows 2FA prompt after omniauth login' do
sign_in_using_saml!
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
enter_code(user.current_otp)
enable_admin_mode_using_saml!
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
# Cannot reuse the TOTP
travel_to(30.seconds.from_now) do
@@ -210,12 +210,12 @@ RSpec.describe 'Admin Mode Login', feature_category: :authentication_and_authori
context 'when two factor authentication is required' do
it 'shows 2FA prompt after ldap login' do
sign_in_using_ldap!(user, provider_label)
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
enter_code(user.current_otp)
enable_admin_mode_using_ldap!(user)
- expect(page).to have_content('Two-Factor Authentication')
+ expect(page).to have_content(_('Enter verification code'))
# Cannot reuse the TOTP
travel_to(30.seconds.from_now) do
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index f4e8941d25a..25f77da4401 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin Mode Logout', :js, feature_category: :authentication_and_authorization do
+RSpec.describe 'Admin Mode Logout', :js, feature_category: :system_access do
include TermsHelper
include UserLoginHelper
include Spec::Support::Helpers::Features::TopNavSpecHelpers
diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb
index f3639fd0800..305927663e9 100644
--- a/spec/features/admin/admin_mode/workers_spec.rb
+++ b/spec/features/admin/admin_mode/workers_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# Test an operation that triggers background jobs requiring administrative rights
-RSpec.describe 'Admin mode for workers', :request_store, feature_category: :authentication_and_authorization do
+RSpec.describe 'Admin mode for workers', :request_store, feature_category: :system_access do
include Spec::Support::Helpers::Features::AdminUsersHelpers
let(:user) { create(:user) }
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index 769ff75b5a2..3c47a991fd1 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin mode', :js, feature_category: :not_owned do
+RSpec.describe 'Admin mode', :js, feature_category: :shared do
include MobileHelpers
include Spec::Support::Helpers::Features::TopNavSpecHelpers
include StubENV
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index f08e6521184..405a254dc84 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -161,4 +161,29 @@ RSpec.describe "Admin::Projects", feature_category: :projects do
expect(page).to have_current_path(dashboard_projects_path, ignore_query: true, url: false)
end
end
+
+ describe 'project edit' do
+ it 'updates project details' do
+ project = create(:project, :private, name: 'Garfield', description: 'Funny Cat')
+
+ visit edit_admin_namespace_project_path({ id: project.to_param, namespace_id: project.namespace.to_param })
+
+ aggregate_failures do
+ expect(page).to have_content(project.name)
+ expect(page).to have_content(project.description)
+ end
+
+ fill_in 'Project name', with: 'Scooby-Doo'
+ fill_in 'Project description (optional)', with: 'Funny Dog'
+
+ click_button 'Save changes'
+
+ visit edit_admin_namespace_project_path({ id: project.to_param, namespace_id: project.namespace.to_param })
+
+ aggregate_failures do
+ expect(page).to have_content('Scooby-Doo')
+ expect(page).to have_content('Funny Dog')
+ end
+ end
+ end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 04dc206f052..d9867c2e704 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -23,8 +23,6 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe "runners creation" do
before do
- stub_feature_flags(create_runner_workflow: true)
-
visit admin_runners_path
end
@@ -35,7 +33,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
describe "runners registration" do
before do
- stub_feature_flags(create_runner_workflow: false)
+ stub_feature_flags(create_runner_workflow_for_admin: false)
visit admin_runners_path
end
@@ -493,6 +491,29 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
+ describe "Runner create page", :js do
+ before do
+ visit new_admin_runner_path
+ end
+
+ context 'when runner is saved' do
+ before do
+ fill_in s_('Runners|Runner description'), with: 'runner-foo'
+ fill_in s_('Runners|Tags'), with: 'tag1'
+ click_on _('Submit')
+ wait_for_requests
+ end
+
+ it 'navigates to registration page and opens install instructions drawer' do
+ expect(page.find('[data-testid="alert-success"]')).to have_content(s_('Runners|Runner created.'))
+ expect(current_url).to match(register_admin_runner_path(Ci::Runner.last))
+
+ click_on 'How do I install GitLab Runner?'
+ expect(page.find('[data-testid="runner-platforms-drawer"]')).to have_content('gitlab-runner install')
+ end
+ end
+ end
+
describe "Runner show page", :js do
let_it_be(:runner) do
create(
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index cad1bf74d2e..77266e65e4c 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -191,7 +191,7 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
visit admin_background_migrations_path
within '#content-body' do
- expect(page).to have_link('Learn more', href: help_page_path('user/admin_area/monitoring/background_migrations'))
+ expect(page).to have_link('Learn more', href: help_page_path('update/background_migrations'))
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 26efed85513..3a1aa36208e 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin updates settings', feature_category: :not_owned do
+RSpec.describe 'Admin updates settings', feature_category: :shared do
include StubENV
include TermsHelper
include UsageDataHelpers
@@ -666,11 +666,11 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
visit network_admin_application_settings_path
page.within('.as-outbound') do
- check 'Allow requests to the local network from web hooks and services'
+ check 'Allow requests to the local network from webhooks and integrations'
# Enabled by default
uncheck 'Allow requests to the local network from system hooks'
# Enabled by default
- uncheck 'Enforce DNS rebinding attack protection'
+ uncheck 'Enforce DNS-rebinding attack protection'
click_button 'Save changes'
end
@@ -762,6 +762,18 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
expect(current_settings.users_get_by_id_limit_allowlist).to eq(%w[someone someone_else])
end
+ it 'changes Projects API rate limits settings' do
+ visit network_admin_application_settings_path
+
+ page.within('.as-projects-api-limits') do
+ fill_in 'Maximum requests per 10 minutes per IP address', with: 100
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.projects_api_rate_limit_unauthenticated).to eq(100)
+ end
+
shared_examples 'regular throttle rate limit settings' do
it 'changes rate limit settings' do
visit network_admin_application_settings_path
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index 6c4a316ae77..21a001f12c3 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin System Info', feature_category: :not_owned do
+RSpec.describe 'Admin System Info', feature_category: :shared do
before do
admin = create(:admin)
sign_in(admin)
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 5e6cc206883..342e23d0cab 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Admin > Users > Impersonation Tokens', :js, feature_category: :authentication_and_authorization do
+RSpec.describe 'Admin > Users > Impersonation Tokens', :js, feature_category: :system_access do
include Spec::Support::Helpers::ModalHelpers
include Spec::Support::Helpers::AccessTokenHelpers
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
index d1adbf59984..274e62defd9 100644
--- a/spec/features/admin_variables_spec.rb
+++ b/spec/features/admin_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Instance variables', :js, feature_category: :pipeline_authoring do
+RSpec.describe 'Instance variables', :js, feature_category: :pipeline_composition do
let(:admin) { create(:admin) }
let(:page_path) { ci_cd_admin_application_settings_path }
@@ -12,9 +12,21 @@ RSpec.describe 'Instance variables', :js, feature_category: :pipeline_authoring
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
+
visit page_path
wait_for_requests
end
- it_behaves_like 'variable list', isAdmin: true
+ context 'when ci_variables_pages FF is enabled' do
+ it_behaves_like 'variable list', is_admin: true
+ it_behaves_like 'variable list pagination', :ci_instance_variable
+ end
+
+ context 'when ci_variables_pages FF is disabled' do
+ before do
+ stub_feature_flags(ci_variables_pages: false)
+ end
+
+ it_behaves_like 'variable list', is_admin: true
+ end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 3e2e391d060..1ea6e079104 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -150,8 +150,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
find('.board .board-list')
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
- evaluate_script("window.scrollTo(0, document.body.scrollHeight)")
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ evaluate_script("[...document.querySelectorAll('.board:nth-child(2) .board-list [data-testid=\"board-card-gl-io\"]')].pop().scrollIntoView()")
end
expect(page).to have_selector('.board-card', count: 20)
@@ -160,8 +159,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
find('.board .board-list')
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
- evaluate_script("window.scrollTo(0, document.body.scrollHeight)")
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ evaluate_script("[...document.querySelectorAll('.board:nth-child(2) .board-list [data-testid=\"board-card-gl-io\"]')].pop().scrollIntoView()")
end
expect(page).to have_selector('.board-card', count: 30)
@@ -170,8 +168,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
find('.board .board-list')
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
- evaluate_script("window.scrollTo(0, document.body.scrollHeight)")
- evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ evaluate_script("[...document.querySelectorAll('.board:nth-child(2) .board-list [data-testid=\"board-card-gl-io\"]')].pop().scrollIntoView()")
end
expect(page).to have_selector('.board-card', count: 38)
@@ -594,7 +591,9 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do
def remove_list
page.within(find('.board:nth-child(2)')) do
- find('button[title="List settings"]').click
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button('Edit list settings')
end
page.within(find('.js-board-settings-sidebar')) do
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index d597c57ac1c..6753f0ea009 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -32,18 +32,23 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
end
it 'displays new issue button' do
- expect(first('.board')).to have_button('New issue', count: 1)
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ expect(first('.board')).to have_button('Create new issue', count: 1)
end
it 'does not display new issue button in closed list' do
page.within('.board:nth-child(3)') do
- expect(page).not_to have_button('New issue')
+ expect(page).not_to have_selector("[data-testid='header-list-actions']")
+ expect(page).not_to have_button('Create new issue')
end
end
it 'shows form when clicking button' do
page.within(first('.board')) do
- click_button 'New issue'
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button 'Create new issue'
expect(page).to have_selector('.board-new-issue-form')
end
@@ -51,7 +56,9 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
it 'hides form when clicking cancel' do
page.within(first('.board')) do
- click_button 'New issue'
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button 'Create new issue'
expect(page).to have_selector('.board-new-issue-form')
@@ -63,7 +70,9 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
it 'creates new issue, places it on top of the list, and opens sidebar' do
page.within(first('.board')) do
- click_button 'New issue'
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button 'Create new issue'
end
page.within(first('.board-new-issue-form')) do
@@ -91,7 +100,9 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
it 'successfuly loads labels to be added to newly created issue' do
page.within(first('.board')) do
- click_button 'New issue'
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button 'Create new issue'
end
page.within(first('.board-new-issue-form')) do
@@ -121,7 +132,9 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
wait_for_all_requests
page.within('.board:nth-child(2)') do
- click_button('New issue')
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button('Create new issue')
page.within(first('.board-new-issue-form')) do
find('.form-control').set('new issue')
@@ -144,12 +157,14 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
end
it 'does not display new issue button in open list' do
- expect(first('.board')).not_to have_button('New issue')
+ expect(page).not_to have_selector("[data-testid='header-list-actions']")
+ expect(first('.board')).not_to have_button('Create new issue')
end
it 'does not display new issue button in label list' do
page.within('.board:nth-child(2)') do
- expect(page).not_to have_button('New issue')
+ expect(page).not_to have_selector("[data-testid='header-list-actions']")
+ expect(page).not_to have_button('Create new issue')
end
end
end
@@ -173,7 +188,8 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
context 'when backlog does not exist' do
it 'does not display new issue button in label list' do
page.within('.board.is-draggable') do
- expect(page).not_to have_button('New issue')
+ expect(page).not_to have_selector("[data-testid='header-list-actions']")
+ expect(page).not_to have_button('Create new issue')
end
end
end
@@ -182,12 +198,14 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
let_it_be(:backlog_list) { create(:backlog_list, board: group_board) }
it 'does not display new issue button in open list' do
- expect(first('.board')).not_to have_button('New issue')
+ expect(page).not_to have_selector("[data-testid='header-list-actions']")
+ expect(first('.board')).not_to have_button('Create new issue')
end
it 'does not display new issue button in label list' do
page.within('.board.is-draggable') do
- expect(page).not_to have_button('New issue')
+ expect(page).not_to have_selector("[data-testid='header-list-actions']")
+ expect(page).not_to have_button('Create new issue')
end
end
end
@@ -205,7 +223,9 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
context 'when backlog does not exist' do
it 'display new issue button in label list' do
- expect(board_list_header).to have_button('New issue')
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ expect(board_list_header).to have_button('Create new issue')
end
end
@@ -214,7 +234,9 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
before do
page.within(board_list_header) do
- click_button 'New issue'
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ click_button 'Create new issue'
end
project_select_dropdown.click
diff --git a/spec/features/breadcrumbs_schema_markup_spec.rb b/spec/features/breadcrumbs_schema_markup_spec.rb
index d924423c9a9..6610519cd24 100644
--- a/spec/features/breadcrumbs_schema_markup_spec.rb
+++ b/spec/features/breadcrumbs_schema_markup_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures, feature_category: :not_owned do
+RSpec.describe 'Breadcrumbs schema markup', :aggregate_failures, feature_category: :shared do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index b2a29c88b68..67baed5dc91 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -44,15 +44,25 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
"#{get_cell_level_selector(contributions)}[title='#{contribution_text}<br /><span class=\"gl-text-gray-300\">#{date}</span>']"
end
+ def get_days_of_week
+ page.all('[data-testid="user-contrib-cell-group"]')[1]
+ .all('[data-testid="user-contrib-cell"]')
+ .map do |node|
+ node[:title].match(/(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)/)[0]
+ end
+ end
+
def push_code_contribution
event = create(:push_event, project: contributed_project, author: user)
- create(:push_event_payload,
- event: event,
- commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce',
- commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- commit_count: 3,
- ref: 'master')
+ create(
+ :push_event_payload,
+ event: event,
+ commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce',
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 3,
+ ref: 'master'
+ )
end
def note_comment_contribution
@@ -70,162 +80,331 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
find('#js-overview .user-calendar-activities', visible: visible).text
end
- before do
- stub_feature_flags(profile_tabs_vue: false)
- sign_in user
- end
-
- describe 'calendar day selection' do
+ shared_context 'when user page is visited' do
before do
visit user.username
- page.find('.js-overview-tab a').click
+ page.click_link('Overview')
wait_for_requests
end
+ end
- it 'displays calendar' do
- expect(find('#js-overview')).to have_css('.js-contrib-calendar')
+ context 'with `profile_tabs_vue` feature flag disabled' do
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ sign_in user
end
- describe 'select calendar day' do
- let(:cells) { page.all('#js-overview .user-contrib-cell') }
+ describe 'calendar day selection' do
+ include_context 'when user page is visited'
- before do
- cells[0].click
- wait_for_requests
- @first_day_activities = selected_day_activities
+ it 'displays calendar' do
+ expect(find('#js-overview')).to have_css('.js-contrib-calendar')
end
- it 'displays calendar day activities' do
- expect(selected_day_activities).not_to be_empty
- end
+ describe 'select calendar day' do
+ let(:cells) { page.all('#js-overview .user-contrib-cell') }
- describe 'select another calendar day' do
before do
- cells[1].click
+ cells[0].click
wait_for_requests
end
- it 'displays different calendar day activities' do
- expect(selected_day_activities).not_to eq(@first_day_activities)
+ it 'displays calendar day activities' do
+ expect(selected_day_activities).not_to be_empty
end
- end
- describe 'deselect calendar day' do
- before do
- cells[0].click
- wait_for_requests
- cells[0].click
+ describe 'select another calendar day' do
+ it 'displays different calendar day activities' do
+ first_day_activities = selected_day_activities
+
+ cells[1].click
+ wait_for_requests
+
+ expect(selected_day_activities).not_to eq(first_day_activities)
+ end
end
- it 'hides calendar day activities' do
- expect(selected_day_activities(visible: false)).to be_empty
+ describe 'deselect calendar day' do
+ before do
+ cells[0].click
+ wait_for_requests
+ cells[0].click
+ end
+
+ it 'hides calendar day activities' do
+ expect(selected_day_activities(visible: false)).to be_empty
+ end
end
end
end
- end
- shared_context 'visit user page' do
- before do
- visit user.username
- page.find('.js-overview-tab a').click
- wait_for_requests
- end
- end
+ describe 'calendar daily activities' do
+ shared_examples 'a day with activity' do |contribution_count:|
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity square for 1 contribution', :sidekiq_inline do
+ expect(find('#js-overview')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
+
+ today = Date.today.strftime(date_format)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+ end
+ end
- describe 'calendar daily activities' do
- shared_examples 'a day with activity' do |contribution_count:|
- include_context 'visit user page'
+ describe '1 issue and 1 work item creation calendar activity' do
+ before do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ WorkItems::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: { title: 'new task' },
+ spam_params: nil
+ ).execute
+ end
- it 'displays calendar activity square for 1 contribution', :sidekiq_might_not_need_inline do
- expect(find('#js-overview')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
+ it_behaves_like 'a day with activity', contribution_count: 2
- today = Date.today.strftime(date_format)
- expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+ describe 'issue title is shown on activity page' do
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity log', :sidekiq_inline do
+ expect(all('#js-overview .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
+ match(/#{issue_title}/),
+ match(/new task/)
+ )
+ end
+ end
end
- end
- describe '1 issue and 1 work item creation calendar activity' do
- before do
- Issues::CreateService.new(container: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
- WorkItems::CreateService.new(
- container: contributed_project,
- current_user: user,
- params: { title: 'new task' },
- spam_params: nil
- ).execute
+ describe '1 comment calendar activity' do
+ before do
+ note_comment_contribution
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 1
end
- it_behaves_like 'a day with activity', contribution_count: 2
+ describe '10 calendar activities' do
+ before do
+ 10.times { push_code_contribution }
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 10
+ end
+
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution
+
+ travel_to(Date.yesterday) do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ end
+ end
+
+ include_context 'when user page is visited'
- describe 'issue title is shown on activity page' do
- include_context 'visit user page'
+ it 'displays calendar activity squares for both days', :sidekiq_inline do
+ expect(find('#js-overview')).to have_selector(get_cell_level_selector(1), count: 2)
+ end
- it 'displays calendar activity log', :sidekiq_inline do
- expect(all('#js-overview .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
- match(/#{issue_title}/),
- match(/new task/)
- )
+ it 'displays calendar activity square for yesterday', :sidekiq_inline do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today' do
+ today = Date.today.strftime(date_format)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, today), count: 1)
end
end
end
- describe '1 comment calendar activity' do
- before do
- note_comment_contribution
+ describe 'on smaller screens' do
+ shared_examples 'hidden activity calendar' do
+ include_context 'when user page is visited'
+
+ it 'hides the activity calender' do
+ expect(find('#js-overview')).not_to have_css('.js-contrib-calendar')
+ end
end
- it_behaves_like 'a day with activity', contribution_count: 1
+ context 'when screen size is xs' do
+ before do
+ resize_screen_xs
+ end
+
+ it_behaves_like 'hidden activity calendar'
+ end
end
- describe '10 calendar activities' do
- before do
- 10.times { push_code_contribution }
+ describe 'first_day_of_week setting' do
+ context 'when first day of the week is set to Monday' do
+ before do
+ stub_application_setting(first_day_of_week: 1)
+ end
+
+ include_context 'when user page is visited'
+
+ it 'shows calendar with Monday as the first day of the week' do
+ expect(get_days_of_week).to eq(%w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday])
+ end
+ end
+
+ context 'when first day of the week is set to Sunday' do
+ before do
+ stub_application_setting(first_day_of_week: 0)
+ end
+
+ include_context 'when user page is visited'
+
+ it 'shows calendar with Sunday as the first day of the week' do
+ expect(get_days_of_week).to eq(%w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday])
+ end
end
+ end
+ end
+
+ context 'with `profile_tabs_vue` feature flag enabled' do
+ before do
+ sign_in user
+ end
- it_behaves_like 'a day with activity', contribution_count: 10
+ include_context 'when user page is visited'
+
+ it 'displays calendar' do
+ expect(page).to have_css('[data-testid="contrib-calendar"]')
end
- describe 'calendar activity on two days' do
- before do
- push_code_contribution
+ describe 'calendar daily activities' do
+ shared_examples 'a day with activity' do |contribution_count:|
+ include_context 'when user page is visited'
- travel_to(Date.yesterday) do
- Issues::CreateService.new(container: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
+ it 'displays calendar activity square for 1 contribution', :sidekiq_inline do
+ expect(page).to have_selector(get_cell_level_selector(contribution_count), count: 1)
+
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
end
end
- include_context 'visit user page'
- it 'displays calendar activity squares for both days', :sidekiq_might_not_need_inline do
- expect(find('#js-overview')).to have_selector(get_cell_level_selector(1), count: 2)
+ describe '1 issue and 1 work item creation calendar activity' do
+ before do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ WorkItems::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: { title: 'new task' },
+ spam_params: nil
+ ).execute
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 2
end
- it 'displays calendar activity square for yesterday', :sidekiq_might_not_need_inline do
- yesterday = Date.yesterday.strftime(date_format)
- expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ describe '1 comment calendar activity' do
+ before do
+ note_comment_contribution
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 1
end
- it 'displays calendar activity square for today' do
- today = Date.today.strftime(date_format)
- expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, today), count: 1)
+ describe '10 calendar activities' do
+ before do
+ 10.times { push_code_contribution }
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 10
+ end
+
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution
+
+ travel_to(Date.yesterday) do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ end
+ end
+
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity squares for both days', :sidekiq_inline do
+ expect(page).to have_selector(get_cell_level_selector(1), count: 2)
+ end
+
+ it 'displays calendar activity square for yesterday', :sidekiq_inline do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today' do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
end
end
- end
- describe 'on smaller screens' do
- shared_examples 'hidden activity calendar' do
- include_context 'visit user page'
+ describe 'on smaller screens' do
+ shared_examples 'hidden activity calendar' do
+ include_context 'when user page is visited'
- it 'hides the activity calender' do
- expect(find('#js-overview')).not_to have_css('.js-contrib-calendar')
+ it 'hides the activity calender' do
+ expect(page).not_to have_css('[data-testid="contrib-calendar"]')
+ end
+ end
+
+ context 'when screen size is xs' do
+ before do
+ resize_screen_xs
+ end
+
+ it_behaves_like 'hidden activity calendar'
end
end
- context 'size xs' do
- before do
- resize_screen_xs
+ describe 'first_day_of_week setting' do
+ context 'when first day of the week is set to Monday' do
+ before do
+ stub_application_setting(first_day_of_week: 1)
+ end
+
+ include_context 'when user page is visited'
+
+ it 'shows calendar with Monday as the first day of the week' do
+ expect(get_days_of_week).to eq(%w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday])
+ end
end
- it_behaves_like 'hidden activity calendar'
+ context 'when first day of the week is set to Sunday' do
+ before do
+ stub_application_setting(first_day_of_week: 0)
+ end
+
+ include_context 'when user page is visited'
+
+ it 'shows calendar with Sunday as the first day of the week' do
+ expect(get_days_of_week).to eq(%w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday])
+ end
+ end
end
end
end
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb
index 15c900592a1..3282a40854d 100644
--- a/spec/features/callouts/registration_enabled_spec.rb
+++ b/spec/features/callouts/registration_enabled_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Registration enabled callout', feature_category: :authentication_and_authorization do
+RSpec.describe 'Registration enabled callout', feature_category: :system_access do
let_it_be(:admin) { create(:admin) }
let_it_be(:non_admin) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb
index a9672569a4a..dd96b763e55 100644
--- a/spec/features/commit_spec.rb
+++ b/spec/features/commit_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- describe "single commit view" do
+ shared_examples "single commit view" do
let(:commit) do
project.repository.commits(nil, limit: 100).find do |commit|
commit.diffs.size > 1
@@ -16,7 +16,6 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
let(:files) { commit.diffs.diff_files.to_a }
before do
- stub_feature_flags(async_commit_diff_files: false)
project.add_maintainer(user)
sign_in(user)
end
@@ -28,15 +27,9 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
visit project_commit_path(project, commit)
end
- it "shows the short commit message" do
+ it "shows the short commit message, number of total changes and stats", :js, :aggregate_failures do
expect(page).to have_content(commit.title)
- end
-
- it "reports the correct number of total changes" do
expect(page).to have_content("Changes #{commit.diffs.size}")
- end
-
- it 'renders diff stats', :js do
expect(page).to have_selector(".diff-stats")
end
@@ -50,23 +43,36 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
visit project_commit_path(project, commit)
end
- it "shows an adjusted count for changed files on this page", :js do
- expect(page).to have_content("Showing 1 changed file")
+ def diff_files_on_page
+ page.all('.files .diff-file').pluck(:id)
end
- it "shows only the first diff on the first page" do
- expect(page).to have_selector(".files ##{files[0].file_hash}")
- expect(page).not_to have_selector(".files ##{files[1].file_hash}")
- end
+ it "shows paginated content and controls to navigate", :js, :aggregate_failures do
+ expect(page).to have_content("Showing 1 changed file")
+
+ wait_for_requests
+
+ expect(diff_files_on_page).to eq([files[0].file_hash])
- it "can navigate to the second page" do
within(".files .gl-pagination") do
click_on("2")
end
- expect(page).not_to have_selector(".files ##{files[0].file_hash}")
- expect(page).to have_selector(".files ##{files[1].file_hash}")
+ wait_for_requests
+
+ expect(diff_files_on_page).to eq([files[1].file_hash])
end
end
end
+
+ it_behaves_like "single commit view"
+
+ context "when super sidebar is enabled" do
+ before do
+ user.update!(use_new_navigation: true)
+ stub_feature_flags(super_sidebar_nav: true)
+ end
+
+ it_behaves_like "single commit view"
+ end
end
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index 2b671d4b3f1..132c8eb7192 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -39,74 +39,5 @@ RSpec.describe 'Contextual sidebar', :js, feature_category: :remote_development
expect(page).to have_selector('.is-showing-fly-out')
end
end
-
- context 'with invite_members_in_side_nav experiment', :experiment do
- it 'allows opening of modal for the candidate experience' do
- stub_experiments(invite_members_in_side_nav: :candidate)
- expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
- .with_context(group: project.group)
- .on_next_instance
-
- visit project_path(project)
-
- page.within '[data-test-id="side-nav-invite-members"' do
- find('[data-test-id="invite-members-button"').click
- end
-
- expect(page).to have_content("You're inviting members to the")
- end
-
- it 'does not have invite members link in side nav for the control experience' do
- stub_experiments(invite_members_in_side_nav: :control)
- expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
- .with_context(group: project.group)
- .on_next_instance
-
- visit project_path(project)
-
- expect(page).not_to have_css('[data-test-id="side-nav-invite-members"')
- end
- end
- end
-
- context 'when context is a group' do
- let_it_be(:user) { create(:user) }
- let_it_be(:group) do
- create(:group).tap do |g|
- g.add_owner(user)
- end
- end
-
- before do
- sign_in(user)
- end
-
- context 'with invite_members_in_side_nav experiment', :experiment do
- it 'allows opening of modal for the candidate experience' do
- stub_experiments(invite_members_in_side_nav: :candidate)
- expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
- .with_context(group: group)
- .on_next_instance
-
- visit group_path(group)
-
- page.within '[data-test-id="side-nav-invite-members"' do
- find('[data-test-id="invite-members-button"').click
- end
-
- expect(page).to have_content("You're inviting members to the")
- end
-
- it 'does not have invite members link in side nav for the control experience' do
- stub_experiments(invite_members_in_side_nav: :control)
- expect(experiment(:invite_members_in_side_nav)).to track(:assignment)
- .with_context(group: group)
- .on_next_instance
-
- visit group_path(group)
-
- expect(page).not_to have_css('[data-test-id="side-nav-invite-members"')
- end
- end
end
end
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
index f5b02a87758..a5d6ba58ffa 100644
--- a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
+++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
@@ -21,9 +21,8 @@ RSpec.describe 'The group dashboard', :js, feature_category: :subgroups do
within_top_nav do
expect(page).to have_button('Projects')
expect(page).to have_button('Groups')
- expect(page).to have_link('Activity')
- expect(page).to have_link('Milestones')
- expect(page).to have_link('Snippets')
+ expect(page).to have_link('Your work')
+ expect(page).to have_link('Explore')
end
end
@@ -36,9 +35,8 @@ RSpec.describe 'The group dashboard', :js, feature_category: :subgroups do
within_top_nav do
expect(page).to have_button('Projects')
expect(page).to have_button('Groups')
- expect(page).not_to have_link('Activity')
- expect(page).not_to have_link('Milestones')
- expect(page).to have_link('Snippets')
+ expect(page).to have_link('Your work')
+ expect(page).to have_link('Explore')
end
end
end
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index a45e0a58ed6..1fb393e1769 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -230,4 +230,11 @@ RSpec.describe 'Dashboard Groups page', :js, feature_category: :subgroups do
expect(page).not_to have_content(another_group.name)
end
end
+
+ it 'links to the "Explore groups" page' do
+ sign_in(user)
+ visit dashboard_groups_path
+
+ expect(page).to have_link("Explore groups", href: explore_groups_path)
+ end
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 779fbb48ddb..eafc41c0f40 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Dashboard Projects', feature_category: :projects do
let_it_be(:user) { create(:user) }
- let_it_be(:project, reload: true) { create(:project, :repository) }
+ let_it_be(:project, reload: true) { create(:project, :repository, creator: build(:user)) } # ensure creator != owner to avoid N+1 false-positive
let_it_be(:project2) { create(:project, :public) }
before do
@@ -20,6 +20,12 @@ RSpec.describe 'Dashboard Projects', feature_category: :projects do
it_behaves_like "a dashboard page with sidebar", :dashboard_projects_path, :projects
+ it 'links to the "Explore projects" page' do
+ visit dashboard_projects_path
+
+ expect(page).to have_link("Explore projects", href: explore_projects_path)
+ end
+
context 'when user has access to the project' do
it 'shows role badge' do
visit dashboard_projects_path
@@ -239,7 +245,7 @@ RSpec.describe 'Dashboard Projects', feature_category: :projects do
create(:ci_pipeline, :with_job, status: :success, project: project, ref: project.default_branch, sha: project.commit.sha)
visit dashboard_projects_path
- control_count = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count
+ control = ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }
new_project = create(:project, :repository, name: 'new project')
create(:ci_pipeline, :with_job, status: :success, project: new_project, ref: new_project.commit.sha)
@@ -247,15 +253,11 @@ RSpec.describe 'Dashboard Projects', feature_category: :projects do
ActiveRecord::QueryRecorder.new { visit dashboard_projects_path }.count
- # There are seven known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037
- # 1. Project#open_issues_count
- # 2. Project#open_merge_requests_count
- # 3. Project#forks_count
- # 4. ProjectsHelper#load_pipeline_status
- # 5. RendersMemberAccess#preload_max_member_access_for_collection
- # 6. User#max_member_access_for_project_ids
- # 7. Ci::CommitWithPipeline#last_pipeline
+ # There are a few known N+1 queries: https://gitlab.com/gitlab-org/gitlab/-/issues/214037
+ # - User#max_member_access_for_project_ids
+ # - ProjectsHelper#load_pipeline_status / Ci::CommitWithPipeline#last_pipeline
+ # - Ci::Pipeline#detailed_status
- expect { visit dashboard_projects_path }.not_to exceed_query_limit(control_count + 7)
+ expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4)
end
end
diff --git a/spec/features/dashboard/root_explore_spec.rb b/spec/features/dashboard/root_explore_spec.rb
index a232ebec68e..9e844f81a29 100644
--- a/spec/features/dashboard/root_explore_spec.rb
+++ b/spec/features/dashboard/root_explore_spec.rb
@@ -2,16 +2,12 @@
require 'spec_helper'
-RSpec.describe 'Root explore', feature_category: :not_owned do
+RSpec.describe 'Root explore', :saas, feature_category: :shared do
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }
let_it_be(:private_project) { create(:project, :private) }
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
context 'when logged in' do
let_it_be(:user) { create(:user) }
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 30587756505..155f7e93961 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard shortcuts', :js, feature_category: :not_owned do
+RSpec.describe 'Dashboard shortcuts', :js, feature_category: :shared do
context 'logged in' do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index ba40290d866..f4234b433f8 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -7,6 +7,13 @@ RSpec.describe 'Dashboard snippets', feature_category: :source_code_management d
it_behaves_like 'a dashboard page with sidebar', :dashboard_snippets_path, :snippets
+ it 'links to the "Explore snippets" page' do
+ sign_in(user)
+ visit dashboard_snippets_path
+
+ expect(page).to have_link("Explore snippets", href: explore_snippets_path)
+ end
+
context 'when the project has snippets' do
let(:project) { create(:project, :public, creator: user) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.first_owner, project: project) }
diff --git a/spec/features/display_system_header_and_footer_bar_spec.rb b/spec/features/display_system_header_and_footer_bar_spec.rb
index 22fd0987418..9b2bf0ef1fa 100644
--- a/spec/features/display_system_header_and_footer_bar_spec.rb
+++ b/spec/features/display_system_header_and_footer_bar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Display system header and footer bar', feature_category: :not_owned do
+RSpec.describe 'Display system header and footer bar', feature_category: :shared do
let(:header_message) { "Foo" }
let(:footer_message) { "Bar" }
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 1f09b01ddec..43dd80187ce 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -19,6 +19,8 @@ RSpec.describe 'Expand and collapse diffs', :js, feature_category: :source_code_
# Ensure that undiffable.md is in .gitattributes
project.repository.copy_gitattributes(branch)
visit project_commit_path(project, project.commit(branch))
+
+ wait_for_requests
end
def file_container(filename)
@@ -222,10 +224,16 @@ RSpec.describe 'Expand and collapse diffs', :js, feature_category: :source_code_
let(:branch) { 'expand-collapse-files' }
# safe-files -> 100 | safe-lines -> 5000 | commit-files -> 105
- it 'does collapsing from the safe number of files to the end on small files' do
- expect(page).to have_link('Expand all')
+ it 'does collapsing from the safe number of files to the end on small files', :aggregate_failures do
+ expect(page).not_to have_link('Expand all')
+ expect(page).to have_selector('.diff-content', count: 20)
+ expect(page).to have_selector('.diff-collapsed', count: 0)
- expect(page).to have_selector('.diff-content', count: 105)
+ visit project_commit_path(project, project.commit(branch), page: 6)
+ wait_for_requests
+
+ expect(page).to have_link('Expand all')
+ expect(page).to have_selector('.diff-content', count: 5)
expect(page).to have_selector('.diff-collapsed', count: 5)
%w(file-95.txt file-96.txt file-97.txt file-98.txt file-99.txt).each do |filename|
diff --git a/spec/features/explore/navbar_spec.rb b/spec/features/explore/navbar_spec.rb
new file mode 100644
index 00000000000..8f281abe6a7
--- /dev/null
+++ b/spec/features/explore/navbar_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe '"Explore" navbar', feature_category: :navigation do
+ include_context '"Explore" navbar structure'
+
+ it_behaves_like 'verified navigation bar' do
+ before do
+ visit explore_projects_path
+ end
+ end
+end
diff --git a/spec/features/frequently_visited_projects_and_groups_spec.rb b/spec/features/frequently_visited_projects_and_groups_spec.rb
index 50e20910e16..19495230795 100644
--- a/spec/features/frequently_visited_projects_and_groups_spec.rb
+++ b/spec/features/frequently_visited_projects_and_groups_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Frequently visited items', :js, feature_category: :not_owned do
+RSpec.describe 'Frequently visited items', :js, feature_category: :shared do
include Spec::Support::Helpers::Features::TopNavSpecHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 117f50aefc6..8644a15a093 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group variables', :js, feature_category: :pipeline_authoring do
+RSpec.describe 'Group variables', :js, feature_category: :pipeline_composition do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', masked: true, group: group) }
@@ -15,5 +15,16 @@ RSpec.describe 'Group variables', :js, feature_category: :pipeline_authoring do
wait_for_requests
end
- it_behaves_like 'variable list'
+ context 'when ci_variables_pages FF is enabled' do
+ it_behaves_like 'variable list'
+ it_behaves_like 'variable list pagination', :ci_group_variable
+ end
+
+ context 'when ci_variables_pages FF is disabled' do
+ before do
+ stub_feature_flags(ci_variables_pages: false)
+ end
+
+ it_behaves_like 'variable list'
+ end
end
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index c451a97bed5..8acf3ffe441 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -25,8 +25,10 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
it 'adds an issue to the backlog' do
page.within(find('.board', match: :first)) do
- issue_title = 'New Issue'
- click_button 'New issue'
+ dropdown = first("[data-testid='header-list-actions']")
+ dropdown.click
+ issue_title = 'Create new issue'
+ click_button issue_title
wait_for_requests
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 5510e73ef0f..2aa70ec1953 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -147,14 +147,14 @@ RSpec.describe 'Edit group settings', feature_category: :subgroups do
selected_group.add_owner(user)
end
- it 'can successfully transfer the group', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384966' do
+ it 'can successfully transfer the group' do
visit edit_group_path(selected_group)
page.within('[data-testid="transfer-locations-dropdown"]') do
click_button _('Select parent group')
fill_in _('Search'), with: target_group_name
wait_for_requests
- click_button target_group_name
+ click_button(target_group_name || 'No parent group')
end
click_button s_('GroupSettings|Transfer group')
@@ -166,7 +166,10 @@ RSpec.describe 'Edit group settings', feature_category: :subgroups do
click_button 'Confirm'
end
- expect(page).to have_text "Group '#{selected_group.name}' was successfully transferred."
+ within('[data-testid="breadcrumb-links"]') do
+ expect(page).to have_content(target_group_name) if target_group_name
+ expect(page).to have_content(selected_group.name)
+ end
expect(current_url).to include(selected_group.reload.full_path)
end
end
@@ -175,7 +178,7 @@ RSpec.describe 'Edit group settings', feature_category: :subgroups do
let(:selected_group) { create(:group, path: 'foo-subgroup', parent: group) }
context 'when transfering to no parent group' do
- let(:target_group_name) { 'No parent group' }
+ let(:target_group_name) { nil }
it_behaves_like 'can transfer the group'
end
diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb
index 8aea18a268b..f6548c035f0 100644
--- a/spec/features/groups/import_export/connect_instance_spec.rb
+++ b/spec/features/groups/import_export/connect_instance_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js, feature_categ
pat = 'demo-pat'
expect(page).to have_content 'Import groups by direct transfer'
- expect(page).to have_content 'Not all related objects are migrated'
+ expect(page).to have_content 'Not all group items are migrated'
fill_in :bulk_import_gitlab_url, with: source_url
fill_in :bulk_import_gitlab_access_token, with: pat
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index 5634122ec16..fa5a14f18b4 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :subgro
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
- expect(page).to have_css('button[aria-haspopup="true"]', text: text)
+ expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
diff --git a/spec/features/groups/new_group_page_spec.rb b/spec/features/groups/new_group_page_spec.rb
index a07c27331d9..6d9513ce84f 100644
--- a/spec/features/groups/new_group_page_spec.rb
+++ b/spec/features/groups/new_group_page_spec.rb
@@ -3,15 +3,14 @@
require 'spec_helper'
RSpec.describe 'New group page', :js, feature_category: :subgroups do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:parent_group) { create(:group) }
before do
+ parent_group.add_owner(user)
sign_in(user)
end
- it_behaves_like 'a dashboard page with sidebar', :new_group_path, :groups
-
describe 'new top level group alert' do
context 'when a user visits the new group page' do
it 'shows the new top level group alert' do
@@ -22,8 +21,6 @@ RSpec.describe 'New group page', :js, feature_category: :subgroups do
end
context 'when a user visits the new sub group page' do
- let(:parent_group) { create(:group) }
-
it 'does not show the new top level group alert' do
visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
@@ -31,4 +28,45 @@ RSpec.describe 'New group page', :js, feature_category: :subgroups do
end
end
end
+
+ describe 'sidebar' do
+ context 'in the current navigation' do
+ before do
+ user.update!(use_new_navigation: false)
+ end
+
+ context 'for a new top-level group' do
+ it_behaves_like 'a dashboard page with sidebar', :new_group_path, :groups
+ end
+
+ context 'for a new subgroup' do
+ it 'shows the group sidebar of the parent group' do
+ visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
+ expect(page).to have_selector(
+ ".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]"
+ )
+ end
+ end
+ end
+
+ context 'in the new navigation' do
+ before do
+ user.update!(use_new_navigation: true)
+ end
+
+ context 'for a new top-level group' do
+ it 'shows the "Your work" navigation' do
+ visit new_group_path
+ expect(page).to have_selector(".super-sidebar .context-switcher-toggle", text: "Your work")
+ end
+ end
+
+ context 'for a new subgroup' do
+ it 'shows the group navigation of the parent group' do
+ visit new_group_path(parent_id: parent_group.id, anchor: 'create-group-pane')
+ expect(page).to have_selector(".super-sidebar .context-switcher-toggle", text: parent_group.name)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/groups/settings/access_tokens_spec.rb b/spec/features/groups/settings/access_tokens_spec.rb
index 1bee3be1ddb..cb92f9abdf5 100644
--- a/spec/features/groups/settings/access_tokens_spec.rb
+++ b/spec/features/groups/settings/access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Group > Settings > Access Tokens', :js, feature_category: :authentication_and_authorization do
+RSpec.describe 'Group > Settings > Access Tokens', :js, feature_category: :system_access do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index 80e2dcd5174..a6c980f539c 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -56,6 +56,14 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
expect(sidebar).to have_link _('Packages and registries')
end
+ it 'passes axe automated accessibility testing', :js do
+ visit_settings_page
+
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within '[data-testid="packages-and-registries-group-settings"]'
+ end
+
it 'has a Duplicate packages section', :js do
visit_settings_page
diff --git a/spec/features/help_dropdown_spec.rb b/spec/features/help_dropdown_spec.rb
index a5c9221ad26..5f1d3a5e2b7 100644
--- a/spec/features/help_dropdown_spec.rb
+++ b/spec/features/help_dropdown_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Help Dropdown", :js, feature_category: :not_owned do
+RSpec.describe "Help Dropdown", :js, feature_category: :shared do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 6c0901d6169..905c5e25f6e 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Help Pages', feature_category: :not_owned do
+RSpec.describe 'Help Pages', feature_category: :shared do
describe 'Get the main help page' do
before do
allow(File).to receive(:read).and_call_original
diff --git a/spec/features/incidents/incident_timeline_events_spec.rb b/spec/features/incidents/incident_timeline_events_spec.rb
index 7404ac64cc9..a4449ee2608 100644
--- a/spec/features/incidents/incident_timeline_events_spec.rb
+++ b/spec/features/incidents/incident_timeline_events_spec.rb
@@ -96,5 +96,6 @@ RSpec.describe 'Incident timeline events', :js, feature_category: :incident_mana
it_behaves_like 'for each incident details route',
'add, edit, and delete timeline events',
- tab_text: s_('Incident|Timeline')
+ tab_text: s_('Incident|Timeline'),
+ tab: 'timeline'
end
diff --git a/spec/features/incidents/user_views_alert_details_spec.rb b/spec/features/incidents/user_views_alert_details_spec.rb
new file mode 100644
index 00000000000..f3d0273071c
--- /dev/null
+++ b/spec/features/incidents/user_views_alert_details_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User uploads alerts to incident', :js, feature_category: :incident_management do
+ let_it_be(:incident) { create(:incident) }
+ let_it_be(:project) { incident.project }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+
+ context 'with alert' do
+ let_it_be(:alert) { create(:alert_management_alert, issue_id: incident.id, project: project) }
+
+ shared_examples 'shows alert tab with details' do
+ specify do
+ expect(page).to have_link(s_('Incident|Alert details'))
+ expect(page).to have_content(alert.title)
+ end
+ end
+
+ it_behaves_like 'for each incident details route',
+ 'shows alert tab with details',
+ tab_text: s_('Incident|Alert details'),
+ tab: 'alerts'
+ end
+
+ context 'with no alerts' do
+ it 'hides the Alert details tab' do
+ sign_in(user)
+ visit project_issue_path(project, incident)
+
+ expect(page).not_to have_link(s_('Incident|Alert details'))
+ end
+ end
+end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 1091bea1ce3..cb7e933e472 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -244,9 +244,8 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
- context 'when soft email confirmation is not enabled' do
+ context 'when email confirmation is not set to `soft`' 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
@@ -261,9 +260,9 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate
end
end
- context 'when soft email confirmation is enabled' do
+ context 'when email confirmation setting is set to `soft`' do
before do
- stub_feature_flags(soft_email_confirmation: true)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 350b0582565..c979aff2147 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe 'issuable list', :js, feature_category: :team_planning do
end
end
- it 'displays a warning if counting the number of issues times out' do
+ it 'displays a warning if counting the number of issues times out', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/393344' do
allow_any_instance_of(IssuesFinder).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
visit_issuable_list(:issue)
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index 33fc9a6fd96..47865d2b6ba 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
it 'the user can not create a comment' do
page.within('#notes') do
expect(page).not_to have_selector('js-main-target-form')
- expect(page.find('.disabled-comment'))
+ expect(page.find('.disabled-comments'))
.to have_content('This issue is locked. Only project members can comment.')
end
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 585740f7782..fa5dd8c893c 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
+ let_it_be(:issue2) { create(:issue, project: project, assignees: [user], milestone: milestone) }
let_it_be(:confidential_issue) { create(:issue, project: project, assignees: [user], milestone: milestone, confidential: true) }
let(:current_user) { user }
@@ -477,14 +478,69 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
end
describe 'inline edit' do
- before do
- visit project_issue_path(project, issue)
+ context 'within issue 1' do
+ before do
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'opens inline edit form with shortcut' do
+ find('body').send_keys('e')
+
+ expect(page).to have_selector('.detail-page-description form')
+ end
+
+ describe 'when user has made no changes' do
+ it 'let user leave the page without warnings' do
+ expected_content = 'Issue created'
+ expect(page).to have_content(expected_content)
+
+ find('body').send_keys('e')
+
+ click_link 'Boards'
+
+ expect(page).not_to have_content(expected_content)
+ end
+ end
+
+ describe 'when user has made changes' do
+ it 'shows a warning and can stay on page' do
+ content = 'new issue content'
+
+ find('body').send_keys('e')
+ fill_in 'issue-description', with: content
+
+ click_link 'Boards'
+
+ page.driver.browser.switch_to.alert.dismiss
+
+ click_button 'Save changes'
+ wait_for_requests
+
+ expect(page).to have_content(content)
+ end
+ end
end
- it 'opens inline edit form with shortcut' do
- find('body').send_keys('e')
+ context 'within issue 2' do
+ before do
+ visit project_issue_path(project, issue2)
+ wait_for_requests
+ end
+
+ describe 'when user has made changes' do
+ it 'shows a warning and can leave page' do
+ content = 'new issue content'
+ find('body').send_keys('e')
+ fill_in 'issue-description', with: content
+
+ click_link 'Boards'
- expect(page).to have_selector('.detail-page-description form')
+ page.driver.browser.switch_to.alert.accept
+
+ expect(page).not_to have_content(content)
+ end
+ end
end
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 20a69c61871..d5f90bb9260 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -48,6 +48,30 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
end
end
+ context 'when issue description has task list items' do
+ before do
+ description = '- [ ] I am a task
+
+| Table |
+|-------|
+| <ul><li>[ ] I am inside a table</li><ul> |'
+ issue.update!(description: description)
+
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows task actions ellipsis button when hovering over the task list item, but not within a table', :aggregate_failures do
+ find('li', text: 'I am a task').hover
+
+ expect(page).to have_button 'Task actions'
+
+ find('li', text: 'I am inside a table').hover
+
+ expect(page).not_to have_button 'Task actions'
+ end
+ end
+
context 'when issue description has xss snippet' do
before do
issue.update!(description: '![xss" onload=alert(1);//](a)')
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 686074f7412..95277caf0f5 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -119,8 +119,6 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite members')
- expect(page).to have_selector('[data-track-action="click_invite_members"]')
- expect(page).to have_selector('[data-track-label="edit_assignee"]')
click_link 'Invite members'
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index ea68f2266b3..e2329e5e287 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
end
end
- context 'service desk issue moved to a project with service desk disabled', :js do
+ context 'service desk issue moved to a project with service desk disabled', :saas, :js do
let(:project_title) { 'service desk disabled project' }
let(:warning_selector) { '.js-alert-moved-from-service-desk-warning' }
let(:namespace) { create(:namespace) }
@@ -106,7 +106,6 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
let(:service_desk_issue) { create(:issue, project: service_desk_project, author: ::User.support_bot) }
before do
- allow(Gitlab).to receive(:com?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index 59e1413fc97..145fa3c4a9e 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -32,6 +32,8 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
end
end
+ it_behaves_like 'edits content using the content editor'
+
it "adds comment with code block" do
code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)"
comment = "```\n#{code_block_content}\n```"
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index bbc14368d82..6325f226ccf 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -113,6 +113,26 @@ RSpec.describe 'User creates branch and merge request on issue page', :js, featu
expect(page).to have_current_path project_tree_path(project, branch_name), ignore_query: true
end
end
+
+ context 'when branch name is invalid' do
+ shared_examples 'has error message' do |dropdown|
+ it 'has error message' do
+ select_dropdown_option(dropdown, 'custom-branch-name w~th ^bad chars?')
+
+ wait_for_requests
+
+ expect(page).to have_text("Can't contain spaces, ~, ^, ?")
+ end
+ end
+
+ context 'when creating a merge request', :sidekiq_might_not_need_inline do
+ it_behaves_like 'has error message', 'create-mr'
+ end
+
+ context 'when creating a branch', :sidekiq_might_not_need_inline do
+ it_behaves_like 'has error message', 'create-branch'
+ end
+ end
end
context "when there is a referenced merge request" do
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index bf2af918f39..06c1b2afdb0 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -111,23 +111,29 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
markdown_field_focused_selector = 'textarea:focus'
click_edit_issue_description
- expect(page).to have_selector(markdown_field_focused_selector)
+ issuable_form = find('[data-testid="issuable-form"]')
- click_on _('View rich text')
- click_on _('Rich text')
+ expect(issuable_form).to have_selector(markdown_field_focused_selector)
- expect(page).not_to have_selector(content_editor_focused_selector)
+ page.within issuable_form do
+ click_on _('Viewing markdown')
+ click_on _('Rich text')
+ end
+
+ expect(issuable_form).not_to have_selector(content_editor_focused_selector)
refresh
click_edit_issue_description
- expect(page).to have_selector(content_editor_focused_selector)
+ expect(issuable_form).to have_selector(content_editor_focused_selector)
- click_on _('View markdown')
- click_on _('Markdown')
+ page.within issuable_form do
+ click_on _('Viewing rich text')
+ click_on _('Markdown')
+ end
- expect(page).not_to have_selector(markdown_field_focused_selector)
+ expect(issuable_form).not_to have_selector(markdown_field_focused_selector)
end
end
diff --git a/spec/features/markdown/observability_spec.rb b/spec/features/markdown/observability_spec.rb
index 86caf3eb1b1..ec414d4396e 100644
--- a/spec/features/markdown/observability_spec.rb
+++ b/spec/features/markdown/observability_spec.rb
@@ -2,82 +2,44 @@
require 'spec_helper'
-RSpec.describe 'Observability rendering', :js do
+RSpec.describe 'Observability rendering', :js, feature_category: :metrics do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:user) { create(:user) }
- let_it_be(:observable_url) { "https://observe.gitlab.com/" }
-
- let_it_be(:expected) do
- %(<iframe src="#{observable_url}?theme=light&amp;kiosk" frameborder="0")
- end
+ let_it_be(:observable_url) { "https://www.gitlab.com/groups/#{group.path}/-/observability/explore?observability_path=/explore?foo=bar" }
+ let_it_be(:expected_observable_url) { "https://observe.gitlab.com/-/#{group.id}/explore?foo=bar" }
before do
- project.add_maintainer(user)
+ stub_config_setting(url: "https://www.gitlab.com")
+ group.add_developer(user)
sign_in(user)
end
- context 'when embedding in an issue' do
- let(:issue) do
- create(:issue, project: project, description: observable_url)
- end
-
- before do
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'renders iframe in description' do
- page.within('.description') do
- expect(page.html).to include(expected)
- end
- end
-
- it 'renders iframe in comment' do
- expect(page).not_to have_css('.note-text')
-
- page.within('.js-main-target-form') do
- fill_in('note[note]', with: observable_url)
- click_button('Comment')
+ context 'when user is a developer of the embedded group' do
+ context 'when embedding in an issue' do
+ let(:issue) do
+ create(:issue, project: project, description: observable_url)
end
- wait_for_requests
-
- page.within('.note-text') do
- expect(page.html).to include(expected)
+ before do
+ visit project_issue_path(project, issue)
+ wait_for_requests
end
- end
- end
-
- context 'when embedding in an MR' do
- let(:merge_request) do
- create(:merge_request, source_project: project, target_project: project, description: observable_url)
- end
- before do
- visit merge_request_path(merge_request)
- wait_for_requests
+ it_behaves_like 'embeds observability'
end
- it 'renders iframe in description' do
- page.within('.description') do
- expect(page.html).to include(expected)
+ context 'when embedding in an MR' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project, description: observable_url)
end
- end
- it 'renders iframe in comment' do
- expect(page).not_to have_css('.note-text')
-
- page.within('.js-main-target-form') do
- fill_in('note[note]', with: observable_url)
- click_button('Comment')
+ before do
+ visit merge_request_path(merge_request)
+ wait_for_requests
end
- wait_for_requests
-
- page.within('.note-text') do
- expect(page.html).to include(expected)
- end
+ it_behaves_like 'embeds observability'
end
end
@@ -96,28 +58,7 @@ RSpec.describe 'Observability rendering', :js do
wait_for_requests
end
- it 'does not render iframe in description' do
- page.within('.description') do
- expect(page.html).not_to include(expected)
- expect(page.html).to include(observable_url)
- end
- end
-
- it 'does not render iframe in comment' do
- expect(page).not_to have_css('.note-text')
-
- page.within('.js-main-target-form') do
- fill_in('note[note]', with: observable_url)
- click_button('Comment')
- end
-
- wait_for_requests
-
- page.within('.note-text') do
- expect(page.html).not_to include(expected)
- expect(page.html).to include(observable_url)
- end
- end
+ it_behaves_like 'does not embed observability'
end
context 'when embedding in an MR' do
@@ -130,28 +71,7 @@ RSpec.describe 'Observability rendering', :js do
wait_for_requests
end
- it 'does not render iframe in description' do
- page.within('.description') do
- expect(page.html).not_to include(expected)
- expect(page.html).to include(observable_url)
- end
- end
-
- it 'does not render iframe in comment' do
- expect(page).not_to have_css('.note-text')
-
- page.within('.js-main-target-form') do
- fill_in('note[note]', with: observable_url)
- click_button('Comment')
- end
-
- wait_for_requests
-
- page.within('.note-text') do
- expect(page.html).not_to include(expected)
- expect(page.html).to include(observable_url)
- end
- end
+ it_behaves_like 'does not embed observability'
end
end
end
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 66b87148eb2..9ab53a00903 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'User comments on a diff', :js, feature_category: :code_review_wo
end
context 'in multiple files' do
- it 'toggles comments' do
+ it 'toggles comments', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/393518' do
first_line_element = find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']").find(:xpath, "..")
first_root_element = first_line_element.ancestor('[data-path]')
click_diff_line(first_line_element)
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 9335615b4c7..e113e305af5 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
@@ -30,6 +30,8 @@ RSpec.describe 'User comments on a merge request', :js, feature_category: :code_
end
end
+ it_behaves_like 'edits content using the content editor'
+
it 'replys to a new comment' do
page.within('.js-main-target-form') do
fill_in('note[note]', with: 'comment 1')
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index 1d7a3fae371..6d3268ffe3a 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -174,7 +174,7 @@ RSpec.describe 'Merge request > User creates image diff notes', :js, feature_cat
end
shared_examples 'onion skin' do
- it 'resets opacity when toggling between view modes' do
+ it 'resets opacity when toggling between view modes', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/393331' do
# Simulate dragging onion-skin slider
drag_and_drop_by(find('.dragger'), -30, 0)
diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
index cf5024ad59e..becbf0ccfa7 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -162,8 +162,6 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite members')
- expect(page).to have_selector('[data-track-action="click_invite_members"]')
- expect(page).to have_selector('[data-track-label="edit_assignee"]')
click_link 'Invite members'
end
diff --git a/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
index 26a9b955e2d..52d058aeabc 100644
--- a/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
@@ -26,8 +26,6 @@ RSpec.describe 'Merge request > User edits reviewers sidebar', :js, feature_cate
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite Members')
- expect(page).to have_selector('[data-track-action="click_invite_members"]')
- expect(page).to have_selector('[data-track-label="edit_reviewer"]')
end
click_link 'Invite Members'
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index 43ce473b407..e09a4569caf 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi
revert_commit
- expect(page).to have_content('Sorry, we cannot revert this merge request automatically.')
+ expect(page).to have_content('Merge request revert failed:')
end
it 'reverts a merge request in a new merge request', :sidekiq_might_not_need_inline do
diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
index 6e6c2cddfbf..06276d2a933 100644
--- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
expect(page).to have_selector(second_discussion_selector, obscured: false)
end
- it 'navigates through active threads' do
+ it 'navigates through active threads', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391912' do
goto_next_thread
goto_next_thread
expect(page).to have_selector(second_discussion_selector, obscured: false)
diff --git a/spec/features/merge_request/user_sees_real_time_reviewers_spec.rb b/spec/features/merge_request/user_sees_real_time_reviewers_spec.rb
new file mode 100644
index 00000000000..e967787d2c7
--- /dev/null
+++ b/spec/features/merge_request/user_sees_real_time_reviewers_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > Real-time reviewers', feature_category: :code_review_workflow do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user) }
+
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'updates in real-time', :js do
+ wait_for_requests
+
+ # Simulate a real-time update of reviewers
+ merge_request.update!(reviewer_ids: [user.id])
+ GraphqlTriggers.merge_request_reviewers_updated(merge_request)
+
+ expect(find('.reviewer')).to have_content(user.name)
+ end
+end
diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb
index e481e3f2dfb..afa57cb0f8f 100644
--- a/spec/features/merge_request/user_views_open_merge_request_spec.rb
+++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb
@@ -7,6 +7,18 @@ RSpec.describe 'User views an open merge request', feature_category: :code_revie
create(:merge_request, source_project: project, target_project: project, description: '# Description header')
end
+ context 'feature flags' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+
+ it 'pushes content_editor_on_issues feature flag to frontend' do
+ stub_feature_flags(content_editor_on_issues: true)
+
+ visit merge_request_path(merge_request)
+
+ expect(page).to have_pushed_frontend_feature_flags(contentEditorOnIssues: true)
+ end
+ end
+
context 'when a merge request does not have repository' do
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index 3171ae89fe6..371c40b40a5 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago,
updated_at: 1.minute.ago)
- @fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.seconds.ago)
+ @fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 20.seconds.ago)
@markdown = create(:merge_request,
title: 'markdown',
@@ -33,7 +33,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
reviewers: [user, user2, user3, user4],
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
- updated_at: 2.minutes.ago)
+ updated_at: 2.minutes.ago,
+ state: 'merged')
@markdown.metrics.update!(merged_at: 10.minutes.ago, latest_closed_at: 10.seconds.ago)
@merge_test = create(:merge_request,
@@ -49,7 +50,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
source_project: project,
source_branch: 'feautre',
created_at: 2.minutes.ago,
- updated_at: 1.minute.ago)
+ updated_at: 1.minute.ago,
+ state: 'merged')
@feature.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.minutes.ago)
end
@@ -79,10 +81,9 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(page).to have_current_path(project_merge_requests_path(project), ignore_query: true)
expect(page).to have_content 'merge-test'
- expect(page).to have_content 'feature'
expect(page).not_to have_content 'fix'
expect(page).not_to have_content 'markdown'
- expect(count_merge_requests).to eq(2)
+ expect(count_merge_requests).to eq(1)
end
it 'filters on a specific assignee' do
@@ -90,8 +91,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(page).not_to have_content 'merge-test'
expect(page).to have_content 'fix'
- expect(page).to have_content 'markdown'
- expect(count_merge_requests).to eq(2)
+ expect(count_merge_requests).to eq(1)
end
it 'sorts by newest' do
@@ -99,35 +99,35 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(first_merge_request).to include('fix')
expect(last_merge_request).to include('merge-test')
- expect(count_merge_requests).to eq(4)
+ expect(count_merge_requests).to eq(2)
end
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
expect(first_merge_request).to include('merge-test')
- expect(count_merge_requests).to eq(4)
+ expect(count_merge_requests).to eq(2)
end
it 'sorts by milestone due date' do
visit_merge_requests(project, sort: sort_value_milestone)
expect(first_merge_request).to include('fix')
- expect(count_merge_requests).to eq(4)
+ expect(count_merge_requests).to eq(2)
end
- it 'sorts by merged at' do
+ it 'ignores sorting by merged at' do
visit_merge_requests(project, sort: sort_value_merged_date)
- expect(first_merge_request).to include('markdown')
- expect(count_merge_requests).to eq(4)
+ expect(first_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(2)
end
it 'sorts by closed at' do
visit_merge_requests(project, sort: sort_value_closed_date)
- expect(first_merge_request).to include('feature')
- expect(count_merge_requests).to eq(4)
+ expect(first_merge_request).to include('fix')
+ expect(count_merge_requests).to eq(2)
end
it 'filters on one label and sorts by milestone due date' do
@@ -141,6 +141,15 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
expect(count_merge_requests).to eq(1)
end
+ context 'when viewing merged merge requests' do
+ it 'sorts by merged at' do
+ visit_merge_requests(project, state: 'merged', sort: sort_value_merged_date)
+
+ expect(first_merge_request).to include('markdown')
+ expect(count_merge_requests).to eq(2)
+ end
+ end
+
context 'while filtering on two labels' do
let(:label) { create(:label, project: project) }
let(:label2) { create(:label, project: project) }
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index d5f987d15c2..c4114b28b47 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category: :not_owned do
+RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category: :shared do
let_it_be_with_reload(:project) { create(:project, :internal, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/features/nav/new_nav_toggle_spec.rb b/spec/features/nav/new_nav_toggle_spec.rb
index 8e5cc7df053..2cdaf12bb15 100644
--- a/spec/features/nav/new_nav_toggle_spec.rb
+++ b/spec/features/nav/new_nav_toggle_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
it 'allows to disable new nav', :aggregate_failures do
within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
- find('button').click
+ click_button "#{user.name} user’s menu"
expect(page).to have_content('Navigation redesign')
toggle = page.find('.gl-toggle.is-checked')
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
index 56f9d373f00..9ac63c26ba0 100644
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ b/spec/features/nav/top_nav_responsive_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
include MobileHelpers
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
let_it_be(:user) { create(:user) }
@@ -20,7 +21,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
context 'when menu is closed' do
it 'has page content and hides responsive menu', :aggregate_failures do
- expect(page).to have_css('.page-title', text: 'Projects')
+ expect(page).to have_css('.page-title', text: 'Explore projects')
expect(page).to have_link('Dashboard', id: 'logo')
expect(page).to have_no_css('.top-nav-responsive')
@@ -33,14 +34,15 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
end
it 'hides everything and shows responsive menu', :aggregate_failures do
- expect(page).to have_no_css('.page-title', text: 'Projects')
+ expect(page).to have_no_css('.page-title', text: 'Explore projects')
expect(page).to have_no_link('Dashboard', id: 'logo')
within '.top-nav-responsive' do
expect(page).to have_link(nil, href: search_path)
expect(page).to have_button('Projects')
expect(page).to have_button('Groups')
- expect(page).to have_link('Snippets', href: dashboard_snippets_path)
+ expect(page).to have_link('Your work', href: dashboard_projects_path)
+ expect(page).to have_link('Explore', href: explore_projects_path)
end
end
@@ -61,10 +63,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit project_path(project)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(project_project_members_path(project))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
@@ -75,10 +79,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit group_path(group)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(group_group_members_path(group))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{group.name} group")
+ end
end
end
@@ -86,7 +92,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
click_button('Menu')
create_new_button.click
- click_link('Invite members')
+ click_button('Invite members')
end
def create_new_button
diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb
index cc20b626e30..d2c0286cb4d 100644
--- a/spec/features/nav/top_nav_spec.rb
+++ b/spec/features/nav/top_nav_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
let_it_be(:user) { create(:user) }
before do
@@ -16,10 +18,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit project_path(project)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(project_project_members_path(project))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
@@ -30,10 +34,12 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
visit group_path(group)
end
- it 'the add menu contains invite members dropdown option and goes to the members page' do
+ it 'the add menu contains invite members dropdown option and opens invite modal' do
invite_members_from_menu
- expect(page).to have_current_path(group_group_members_path(group))
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{group.name} group")
+ end
end
end
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 a83b5a81a41..b3ba0a874e9 100644
--- a/spec/features/populate_new_pipeline_vars_with_params_spec.rb
+++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Populate new pipeline CI variables with url params", :js, feature_category: :pipeline_authoring do
+RSpec.describe "Populate new pipeline CI variables with url params", :js, feature_category: :pipeline_composition do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:page_path) { new_project_pipeline_path(project) }
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
index 299ecdb6032..9e1bd69a239 100644
--- a/spec/features/profiles/chat_names_spec.rb
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -3,8 +3,7 @@
require 'spec_helper'
RSpec.describe 'Profile > Chat', feature_category: :user_profile do
- let(:user) { create(:user) }
- let(:integration) { create(:integration) }
+ let_it_be(:user) { create(:user) }
before do
sign_in(user)
@@ -60,7 +59,7 @@ RSpec.describe 'Profile > Chat', feature_category: :user_profile do
end
describe 'visits chat accounts' do
- let!(:chat_name) { create(:chat_name, user: user, integration: integration) }
+ let_it_be(:chat_name) { create(:chat_name, user: user) }
before do
visit profile_chat_names_path
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index 0fc59f21489..f39d9ddaf56 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -37,12 +37,13 @@ RSpec.describe 'Profile > GPG Keys', feature_category: :user_profile do
end
it 'user sees their key' do
- create(:gpg_key, user: user, key: GpgHelpers::User2.public_key)
+ gpg_key = create(:gpg_key, user: user, key: GpgHelpers::User2.public_key)
visit profile_gpg_keys_path
expect(page).to have_content('bette.cartwright@example.com Verified')
expect(page).to have_content('bette.cartwright@example.net Unverified')
expect(page).to have_content(GpgHelpers::User2.fingerprint)
+ expect(page).to have_selector('time.js-timeago', text: gpg_key.created_at.strftime('%b %d, %Y'))
end
it 'user removes a key via the key index' do
diff --git a/spec/features/profiles/user_creates_saved_reply_spec.rb b/spec/features/profiles/user_creates_saved_reply_spec.rb
new file mode 100644
index 00000000000..1d851b5cea0
--- /dev/null
+++ b/spec/features/profiles/user_creates_saved_reply_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Profile > Saved replies > User creates saved reply', :js,
+ feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit profile_saved_replies_path
+
+ wait_for_requests
+ end
+
+ it 'shows the user a list of their saved replies' do
+ find('[data-testid="saved-reply-name-input"]').set('test')
+ find('[data-testid="saved-reply-content-input"]').set('Test content')
+
+ click_button 'Save'
+
+ wait_for_requests
+
+ expect(page).to have_content('My saved replies (1)')
+ expect(page).to have_content('test')
+ expect(page).to have_content('Test content')
+ end
+end
diff --git a/spec/features/profiles/user_deletes_saved_reply_spec.rb b/spec/features/profiles/user_deletes_saved_reply_spec.rb
new file mode 100644
index 00000000000..35bd6018ee3
--- /dev/null
+++ b/spec/features/profiles/user_deletes_saved_reply_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Profile > Saved replies > User deletes saved reply', :js,
+ feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:saved_reply) { create(:saved_reply, user: user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the user a list of their saved replies' do
+ visit profile_saved_replies_path
+
+ find('[data-testid="saved-reply-delete-btn"]').click
+
+ page.within('.gl-modal') do
+ click_button 'Delete'
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_content(saved_reply.name)
+ end
+end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 3819723cc09..196134a0bda 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -97,6 +97,26 @@ RSpec.describe 'User edit profile', feature_category: :user_profile do
expect(page).to have_content('Website url is not a valid URL')
end
+ it 'validates that the dicord id has a valid length', :js do
+ valid_dicord_id = '123456789123456789'
+ too_short_discord_id = '123456'
+ too_long_discord_id = '123456789abcdefghijkl'
+
+ fill_in 'user_discord', with: too_short_discord_id
+ expect(page).to have_content('Discord ID is too short')
+
+ fill_in 'user_discord', with: too_long_discord_id
+ expect(page).to have_content('Discord ID is too long')
+
+ fill_in 'user_discord', with: valid_dicord_id
+
+ submit_settings
+
+ expect(user.reload).to have_attributes(
+ discord: valid_dicord_id
+ )
+ end
+
describe 'when I change my email', :js do
before do
user.send_reset_password_instructions
diff --git a/spec/features/profiles/user_updates_saved_reply_spec.rb b/spec/features/profiles/user_updates_saved_reply_spec.rb
new file mode 100644
index 00000000000..e341076ed0a
--- /dev/null
+++ b/spec/features/profiles/user_updates_saved_reply_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Profile > Saved replies > User updated saved reply', :js,
+ feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:saved_reply) { create(:saved_reply, user: user) }
+
+ before do
+ sign_in(user)
+
+ visit profile_saved_replies_path
+
+ wait_for_requests
+ end
+
+ it 'shows the user a list of their saved replies' do
+ find('[data-testid="saved-reply-edit-btn"]').click
+ find('[data-testid="saved-reply-name-input"]').set('test')
+
+ click_button 'Save'
+
+ wait_for_requests
+
+ expect(page).to have_selector('[data-testid="saved-reply-name"]', text: 'test')
+ end
+end
diff --git a/spec/features/profiles/user_uses_saved_reply_spec.rb b/spec/features/profiles/user_uses_saved_reply_spec.rb
new file mode 100644
index 00000000000..f9a4f4a7fa6
--- /dev/null
+++ b/spec/features/profiles/user_uses_saved_reply_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User uses saved reply', :js,
+ feature_category: :user_profile do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:saved_reply) { create(:saved_reply, user: user) }
+
+ before do
+ project.add_owner(user)
+
+ sign_in(user)
+ end
+
+ it 'applies saved reply' do
+ visit project_merge_request_path(merge_request.project, merge_request)
+
+ find('[data-testid="saved-replies-dropdown-toggle"]').click
+
+ wait_for_requests
+
+ find('[data-testid="saved-reply-dropdown-item"]').click
+
+ expect(find('.note-textarea').value).to eq(saved_reply.content)
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
index 90f24c5b866..ac0ed91468c 100644
--- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'User visits the authentication log', feature_category: :user_pro
it 'shows correct menu item' do
visit(audit_log_profile_path)
- expect(page).to have_active_navigation('Authentication log')
+ expect(page).to have_active_navigation('Authentication Log')
end
end
diff --git a/spec/features/project_group_variables_spec.rb b/spec/features/project_group_variables_spec.rb
index 0e1e6e49c6d..8d600edadde 100644
--- a/spec/features/project_group_variables_spec.rb
+++ b/spec/features/project_group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project group variables', :js, feature_category: :pipeline_authoring do
+RSpec.describe 'Project group variables', :js, feature_category: :pipeline_composition do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 1a951980141..69b8408dcd6 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project variables', :js, feature_category: :pipeline_authoring do
+RSpec.describe 'Project variables', :js, feature_category: :pipeline_composition do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value', masked: true) }
@@ -16,7 +16,18 @@ RSpec.describe 'Project variables', :js, feature_category: :pipeline_authoring d
wait_for_requests
end
- it_behaves_like 'variable list'
+ context 'when ci_variables_pages FF is enabled' do
+ it_behaves_like 'variable list'
+ it_behaves_like 'variable list pagination', :ci_variable
+ end
+
+ context 'when ci_variables_pages FF is disabled' do
+ before do
+ stub_feature_flags(ci_variables_pages: false)
+ end
+
+ it_behaves_like 'variable list'
+ end
it 'adds a new variable with an environment scope' do
click_button('Add variable')
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index e6bd4b22b0a..c9e4aabe72a 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -43,13 +43,46 @@ RSpec.describe 'list of badges', feature_category: :continuous_integration do
it 'user changes current ref of build status badge', :js do
page.within('.pipeline-status') do
- first('.js-project-refs-dropdown').click
+ find('.ref-selector').click
+ wait_for_requests
- page.within '.project-refs-form' do
- click_link 'improve/awesome'
+ page.within('.ref-selector') do
+ fill_in 'Search by Git revision', with: 'improve/awesome'
+ wait_for_requests
+ find('li', text: 'improve/awesome', match: :prefer_exact).click
end
expect(page).to have_content 'badges/improve/awesome/pipeline.svg'
end
end
+
+ it 'user changes current ref of coverage status badge', :js do
+ page.within('.coverage-report') do
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ fill_in 'Search by Git revision', with: 'improve/awesome'
+ wait_for_requests
+ find('li', text: 'improve/awesome', match: :prefer_exact).click
+ end
+
+ expect(page).to have_content 'badges/improve/awesome/coverage.svg'
+ end
+ end
+
+ it 'user changes current ref of latest release status badge', :js do
+ page.within('.Latest-Release') do
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ fill_in 'Search by Git revision', with: 'improve/awesome'
+ wait_for_requests
+ find('li', text: 'improve/awesome', match: :prefer_exact).click
+ end
+
+ expect(page).to have_content '-/badges/release.svg'
+ end
+ end
end
diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb
index 27b7c6ef2d5..d3558af81b8 100644
--- a/spec/features/projects/blobs/blame_spec.rb
+++ b/spec/features/projects/blobs/blame_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
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')
+ expect(page).not_to have_link _('Show full blame')
end
end
@@ -53,7 +53,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
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_link _('Show full blame')
expect(page).to have_css('#L1')
expect(page).not_to have_css('#L3')
@@ -85,19 +85,42 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
end
end
- context 'when user clicks on View entire blame button' do
+ shared_examples 'a full blame page' do
+ context 'when user clicks on Show full blame button' do
+ before do
+ visit_blob_blame(path)
+ click_link _('Show full blame')
+ end
+
+ it 'displays the blame page without pagination' do
+ within '[data-testid="blob-content-holder"]' do
+ expect(page).to have_css('#L1')
+ expect(page).to have_css('#L667')
+ expect(page).not_to have_css('.gl-pagination')
+ end
+ end
+ end
+ end
+
+ context 'when streaming is disabled' do
before do
- visit_blob_blame(path)
+ stub_feature_flags(blame_page_streaming: false)
end
- it 'displays the blame page without pagination' do
- within '[data-testid="blob-content-holder"]' do
- click_link _('View entire blame')
+ it_behaves_like 'a full blame page'
+ end
- expect(page).to have_css('#L1')
- expect(page).to have_css('#L3')
- expect(page).not_to have_css('.gl-pagination')
- end
+ context 'when streaming is enabled' do
+ before do
+ stub_const('Projects::BlameService::STREAMING_PER_PAGE', 50)
+ end
+
+ it_behaves_like 'a full blame page'
+
+ it 'shows loading text' do
+ visit_blob_blame(path)
+ click_link _('Show full blame')
+ expect(page).to have_text('Loading full blame...')
end
end
@@ -112,7 +135,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
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')
+ expect(page).not_to have_link _('Show full blame')
end
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 7faf0e1a6b1..f9e3ff1670c 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -137,11 +137,13 @@ RSpec.describe 'File blob', :js, feature_category: :projects do
context 'when ref switch' do
def switch_ref_to(ref_name)
- first('[data-testid="branches-select"]').click
+ find('.ref-selector').click
+ wait_for_requests
- page.within '.project-refs-form' do
- click_link ref_name
+ page.within('.ref-selector') do
+ fill_in 'Search by Git revision', with: ref_name
wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index fc7833809b3..e1f1a63565c 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -201,6 +201,12 @@ RSpec.describe 'Branches', feature_category: :projects do
end
end
+ describe 'Link to branch rules' do
+ it 'does not have possibility to navigate to branch rules', :js do
+ expect(page).not_to have_content(s_("Branches|View branch rules"))
+ end
+ end
+
context 'on project with 0 branch' do
let(:project) { create(:project, :public, :empty_repo) }
let(:repository) { project.repository }
@@ -239,6 +245,17 @@ RSpec.describe 'Branches', feature_category: :projects do
expect(page).not_to have_content 'Merge request'
end
end
+
+ describe 'Navigate to branch rules from branches page' do
+ it 'shows repository settings page with Branch rules section expanded' do
+ visit project_branches_path(project)
+
+ view_branch_rules
+
+ expect(page).to have_content(
+ _('Define rules for who can push, merge, and the required approvals for each branch.'))
+ end
+ end
end
end
@@ -353,4 +370,11 @@ RSpec.describe 'Branches', feature_category: :projects do
click_button 'Yes, delete branch'
end
end
+
+ def view_branch_rules
+ page.within('.nav-controls') do
+ click_link s_("Branches|View branch rules")
+ end
+ wait_for_requests
+ end
end
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index 536152626af..ed03491d69a 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_authoring do
+RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
let(:project) { create(:project_empty_repo, :public) }
diff --git a/spec/features/projects/ci/lint_spec.rb b/spec/features/projects/ci/lint_spec.rb
index 4fea07b18bc..aa9556761c6 100644
--- a/spec/features/projects/ci/lint_spec.rb
+++ b/spec/features/projects/ci/lint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'CI Lint', :js, feature_category: :pipeline_authoring do
+RSpec.describe 'CI Lint', :js, feature_category: :pipeline_composition do
include Spec::Support::Helpers::Features::SourceEditorSpecHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 93ce851521f..b608fc953f3 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe 'Cherry-pick Commits', :js, feature_category: :source_code_manage
cherry_pick_commit
- expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
+ expect(page).to have_content('Commit cherry-pick failed:')
end
end
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
index 8c7b8e6ba32..4d2abf55675 100644
--- a/spec/features/projects/commit/user_reverts_commit_spec.rb
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'User reverts a commit', :js, feature_category: :source_code_mana
revert_commit
- expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
+ expect(page).to have_content('Commit revert failed:')
end
end
diff --git a/spec/features/projects/integrations/apple_app_store_spec.rb b/spec/features/projects/integrations/apple_app_store_spec.rb
new file mode 100644
index 00000000000..b6dc6557e20
--- /dev/null
+++ b/spec/features/projects/integrations/apple_app_store_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do
+ include_context 'project integration activation'
+
+ it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do
+ visit_project_integration('Apple App Store Connect')
+
+ expect(page).to have_content('Drag your Private Key file here or click to upload.')
+ expect(page).not_to have_content('auth_key.p8')
+
+ find("input[name='service[dropzone_file_name]']",
+ visible: false).set(Rails.root.join('spec/fixtures/auth_key.p8'))
+
+ expect(find("input[name='service[app_store_private_key]']",
+ visible: false).value).to eq(File.read(Rails.root.join('spec/fixtures/auth_key.p8')))
+ expect(find("input[name='service[app_store_private_key_file_name]']", visible: false).value).to eq('auth_key.p8')
+
+ expect(page).not_to have_content('Drag your Private Key file here or click to upload.')
+ expect(page).to have_content('auth_key.p8')
+ end
+end
diff --git a/spec/features/projects/integrations/google_play_spec.rb b/spec/features/projects/integrations/google_play_spec.rb
new file mode 100644
index 00000000000..5db4bc8809f
--- /dev/null
+++ b/spec/features/projects/integrations/google_play_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do
+ include_context 'project integration activation'
+
+ it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do
+ visit_project_integration('Google Play')
+
+ expect(page).to have_content('Drag your key file here or click to upload.')
+ expect(page).not_to have_content('service_account.json')
+
+ find("input[name='service[dropzone_file_name]']",
+ visible: false).set(Rails.root.join('spec/fixtures/service_account.json'))
+
+ expect(find("input[name='service[service_account_key]']",
+ visible: false).value).to eq(File.read(Rails.root.join('spec/fixtures/service_account.json')))
+ expect(find("input[name='service[service_account_key_file_name]']",
+ visible: false).value).to eq('service_account.json')
+
+ expect(page).not_to have_content('Drag your key file here or click to upload.')
+ expect(page).to have_content('service_account.json')
+ end
+end
diff --git a/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
index 16c7a3ff226..07cb138c414 100644
--- a/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe 'Set up Mattermost slash commands', :js, feature_category: :integ
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
- expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ expect(token_placeholder).to eq('')
end
end
end
diff --git a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
index ec00dcaf046..01c202baf70 100644
--- a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe 'User activates Slack notifications', :js, feature_category: :int
context 'when integration is not configured yet' do
before do
- stub_feature_flags(integration_slack_app_notifications: false)
visit_project_integration('Slack notifications')
end
diff --git a/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
index 0f6d721565e..38491501c65 100644
--- a/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Slack slash commands', :js, feature_category: :integrations do
it 'shows a token placeholder' do
token_placeholder = find_field('Token')['placeholder']
- expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ expect(token_placeholder).to eq('')
end
it 'shows a help message' do
diff --git a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
index a9e0fce1a1c..e4394010e8c 100644
--- a/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
+++ b/spec/features/projects/jobs/user_triggers_manual_job_with_variables_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'User triggers manual job with variables', :js, feature_category:
find("[data-testid='ci-variable-value']").set('key_value')
end
- find("[data-testid='trigger-manual-job-btn']").click
+ find("[data-testid='run-manual-job-btn']").click
wait_for_requests
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 67389fdda8a..07b8f8339eb 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1065,16 +1065,19 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :proj
end
context "Build from other project" do
+ let(:other_job_download_path) { download_project_job_artifacts_path(project, job2) }
+
before do
create(:ci_job_artifact, :archive, file: artifacts_file, job: job2)
end
- it do
- requests = inspect_requests do
- visit download_project_job_artifacts_path(project, job2)
- end
+ it 'receive 404 from download request', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391632' do
+ requests = inspect_requests { visit other_job_download_path }
+
+ request = requests.find { |request| request.url == other_job_download_path }
- expect(requests.first.status_code).to eq(404)
+ expect(request).to be_present
+ expect(request.status_code).to eq(404)
end
end
end
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
index 6df1e974f42..78fad9b0b55 100644
--- a/spec/features/projects/members/sorting_spec.rb
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :subgroups
def expect_sort_by(text, sort_direction)
within('[data-testid="members-sort-dropdown"]') do
- expect(page).to have_css('button[aria-haspopup="true"]', text: text)
+ expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 6090d132e3a..03ad5f9a292 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :projects do
insert_package_nav(_('Deployments'))
insert_infrastructure_registry_nav
insert_infrastructure_google_cloud_nav
+ insert_infrastructure_aws_nav
end
it_behaves_like 'verified navigation bar' do
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index c6a6ee68185..439ae4275ae 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -578,4 +578,53 @@ RSpec.describe 'New project', :js, feature_category: :projects do
it_behaves_like 'has instructions to enable OAuth'
end
end
+
+ describe 'sidebar' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:parent_group) { create(:group) }
+
+ before do
+ parent_group.add_owner(user)
+ sign_in(user)
+ end
+
+ context 'in the current navigation' do
+ before do
+ user.update!(use_new_navigation: false)
+ end
+
+ context 'for a new top-level project' do
+ it_behaves_like 'a dashboard page with sidebar', :new_project_path, :projects
+ end
+
+ context 'for a new group project' do
+ it 'shows the group sidebar of the parent group' do
+ visit new_project_path(namespace_id: parent_group.id)
+ expect(page).to have_selector(".nav-sidebar[aria-label=\"Group navigation\"] .context-header[title=\"#{parent_group.name}\"]")
+ end
+ end
+ end
+
+ context 'in the new navigation' do
+ before do
+ parent_group.add_owner(user)
+ user.update!(use_new_navigation: true)
+ sign_in(user)
+ end
+
+ context 'for a new top-level project' do
+ it 'shows the "Your work" navigation' do
+ visit new_project_path
+ expect(page).to have_selector(".super-sidebar .context-switcher-toggle", text: "Your work")
+ end
+ end
+
+ context 'for a new group project' do
+ it 'shows the group sidebar of the parent group' do
+ visit new_project_path(namespace_id: parent_group.id)
+ expect(page).to have_selector(".super-sidebar .context-switcher-toggle", text: parent_group.name)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
index a7da59200e9..16e64ade665 100644
--- a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb
@@ -48,13 +48,13 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled, feature_categor
expect(domain.auto_ssl_enabled).to eq false
expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false'
- expect(page).to have_selector '.card-header', text: 'Certificate'
+ expect(page).to have_selector '.gl-card-header', text: 'Certificate'
expect(page).to have_text domain.subject
find('.js-auto-ssl-toggle-container .js-project-feature-toggle button').click
expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
- expect(page).not_to have_selector '.card-header', text: 'Certificate'
+ expect(page).not_to have_selector '.gl-card-header', text: 'Certificate'
expect(page).not_to have_text domain.subject
click_on 'Save Changes'
@@ -108,7 +108,7 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled, feature_categor
it 'user do not see private key' do
visit project_pages_domain_path(project, domain)
- expect(page).not_to have_selector '.card-header', text: 'Certificate'
+ expect(page).not_to have_selector '.gl-card-header', text: 'Certificate'
expect(page).not_to have_text domain.subject
end
end
@@ -131,16 +131,16 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled, feature_categor
it 'user sees certificate subject' do
visit project_pages_domain_path(project, domain)
- expect(page).to have_selector '.card-header', text: 'Certificate'
+ expect(page).to have_selector '.gl-card-header', text: 'Certificate'
expect(page).to have_text domain.subject
end
it 'user can delete the certificate', :js do
visit project_pages_domain_path(project, domain)
- expect(page).to have_selector '.card-header', text: 'Certificate'
+ expect(page).to have_selector '.gl-card-header', text: 'Certificate'
expect(page).to have_text domain.subject
- within('.card') { click_on 'Remove' }
+ within('.gl-card') { click_on 'Remove' }
accept_gl_confirm(button_text: 'Remove certificate')
expect(page).to have_field 'Certificate (PEM)', with: ''
expect(page).to have_field 'Key (PEM)', with: ''
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 3ede76d3360..acb2af07e50 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -193,7 +193,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
save_pipeline_schedule
end
- it 'user sees the new variable in edit window' do
+ it 'user sees the new variable in edit window', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397040' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
page.within('.ci-variable-list') do
expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 343c7f53022..098d1201939 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -113,6 +113,50 @@ RSpec.describe 'Pipeline', :js, feature_category: :projects do
end
end
+ describe 'pipeline stats text' do
+ let(:finished_pipeline) do
+ create(:ci_pipeline, :success, project: project,
+ ref: 'master', sha: project.commit.id, user: user)
+ end
+
+ before do
+ finished_pipeline.update!(started_at: "2023-01-01 01:01:05", created_at: "2023-01-01 01:01:01",
+ finished_at: "2023-01-01 01:01:10", duration: 9)
+ end
+
+ context 'pipeline has finished' do
+ it 'shows pipeline stats with flag on' do
+ visit project_pipeline_path(project, finished_pipeline)
+
+ within '.pipeline-info' do
+ expect(page).to have_content("in #{finished_pipeline.duration} seconds")
+ expect(page).to have_content("and was queued for #{finished_pipeline.queued_duration} seconds")
+ end
+ end
+
+ it 'shows pipeline stats with flag off' do
+ stub_feature_flags(refactor_ci_minutes_consumption: false)
+
+ visit project_pipeline_path(project, finished_pipeline)
+
+ within '.pipeline-info' do
+ expect(page).to have_content("in #{finished_pipeline.duration} seconds " \
+ "and was queued for #{finished_pipeline.queued_duration} seconds")
+ end
+ end
+ end
+
+ context 'pipeline has not finished' do
+ it 'does not show pipeline stats' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_selector('[data-testid="pipeline-stats-text"]')
+ end
+ end
+ end
+ end
+
describe 'related merge requests' do
context 'when there are no related merge requests' do
it 'shows a "no related merge requests" message' do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b5f640f1cca..c46605fa9a8 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -278,6 +278,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
end
before do
+ stub_feature_flags(lazy_load_pipeline_dropdown_actions: false)
visit_project_pipelines
end
@@ -312,6 +313,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
end
before do
+ stub_feature_flags(lazy_load_pipeline_dropdown_actions: false)
visit_project_pipelines
end
@@ -695,7 +697,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
end
context 'when variables are specified' do
- it 'creates a new pipeline with variables', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a new pipeline with variables' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
find("[data-testid='pipeline-form-ci-variable-value']").set('value')
@@ -721,7 +723,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
it { expect(page).to have_content('Missing CI config file') }
- it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
stub_ci_pipeline_to_return_yaml_file
expect do
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
index 12e14f5193f..a38c10c6bab 100644
--- a/spec/features/projects/settings/access_tokens_spec.rb
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :credential_management do
+RSpec.describe 'Project > Settings > Access Tokens', :js, feature_category: :user_management do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index 4b553b57331..900f18bf49e 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Settings > For a forked project', :js, feature_category: :projects do
+ include ListboxHelpers
+
let_it_be(:project) { create(:project, :repository, create_templates: :issue) }
let(:user) { project.first_owner }
@@ -47,7 +49,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego
check(create_issue)
uncheck(send_email)
click_on('No template selected')
- click_on('bug')
+ select_listbox_item('bug')
save_form
click_settings_tab
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 d4c1fe4d43e..57aa3a56c6d 100644
--- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
+++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb
@@ -38,6 +38,15 @@ feature_category: :projects do
expect(section).to have_text 'Clean up image tags'
end
+ it 'passes axe automated accessibility testing' do
+ subject
+
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within('[data-testid="container-expiration-policy-project-settings"]')
+ .skipping :'link-in-text-block'
+ end
+
it 'saves cleanup policy submit the form' do
subject
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 072b5f7f3b0..628fa23afdc 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -21,6 +21,15 @@ feature_category: :projects do
end
context 'as owner', :js do
+ it 'passes axe automated accessibility testing' do
+ subject
+
+ wait_for_requests
+
+ expect(page).to be_axe_clean.within('[data-testid="packages-and-registries-project-settings"]')
+ .skipping :'link-in-text-block'
+ end
+
it 'shows active tab on sidebar' do
subject
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index 5daa5b98b6e..64af25aea28 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -91,23 +91,4 @@ RSpec.describe 'User changes public project visibility', :js, feature_category:
it_behaves_like 'does not require confirmation'
end
-
- context 'with unlink_fork_network_upon_visibility_decrease = false' do
- let(:project) { create(:project, :empty_repo, :public) }
-
- before do
- stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
-
- fork_project(project, project.first_owner)
-
- sign_in(project.first_owner)
-
- visit edit_project_path(project)
-
- # https://gitlab.com/gitlab-org/gitlab/-/issues/381259
- allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
- end
-
- it_behaves_like 'does not require confirmation'
- end
end
diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/projects/work_items/work_item_children_spec.rb
index f41fb86d13c..43a6b2771f6 100644
--- a/spec/features/work_items/work_item_children_spec.rb
+++ b/spec/features/projects/work_items/work_item_children_spec.rb
@@ -132,5 +132,48 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
end
end
end
+
+ context 'in work item metadata' do
+ let_it_be(:label) { create(:label, title: 'Label 1', project: project) }
+ let_it_be(:milestone) { create(:milestone, project: project, title: 'v1') }
+ let_it_be(:task) do
+ create(
+ :work_item,
+ :task,
+ project: project,
+ labels: [label],
+ assignees: [user],
+ milestone: milestone
+ )
+ end
+
+ before do
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+ end
+
+ it 'displays labels, milestone and assignee for work item children', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ click_button 'Add'
+ click_button 'Existing task'
+
+ find('[data-testid="work-item-token-select-input"]').set(task.title)
+ wait_for_all_requests
+ click_button task.title
+
+ click_button 'Add task'
+
+ wait_for_all_requests
+ end
+
+ page.within('[data-testid="links-child"]') do
+ expect(page).to have_content(task.title)
+ expect(page).to have_content(label.title)
+ expect(page).to have_link(user.name)
+ expect(page).to have_content(milestone.title)
+ end
+ end
+ end
end
end
diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb
new file mode 100644
index 00000000000..d0d458350b5
--- /dev/null
+++ b/spec/features/projects/work_items/work_item_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Work item', :js, feature_category: :team_planning do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:milestones) { create_list(:milestone, 25, project: project) }
+
+ context 'for signed in user' do
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ context 'with internal id' do
+ before do
+ visit project_work_items_path(project, work_items_path: work_item.iid, iid_path: true)
+ end
+
+ it_behaves_like 'work items title'
+ it_behaves_like 'work items status'
+ it_behaves_like 'work items assignees'
+ it_behaves_like 'work items labels'
+ it_behaves_like 'work items comments'
+ it_behaves_like 'work items description'
+ it_behaves_like 'work items milestone'
+ end
+
+ context 'with global id' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ visit project_work_items_path(project, work_items_path: work_item.id)
+ end
+
+ it_behaves_like 'work items status'
+ it_behaves_like 'work items assignees'
+ it_behaves_like 'work items labels'
+ it_behaves_like 'work items comments'
+ it_behaves_like 'work items description'
+ end
+ end
+
+ context 'for signed in owner' do
+ before do
+ project.add_owner(user)
+
+ sign_in(user)
+
+ visit project_work_items_path(project, work_items_path: work_item.id)
+ end
+
+ it_behaves_like 'work items invite members'
+ end
+end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c2058a5c345..45315f53fd6 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -16,8 +16,8 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
it "allows creating explicit protected tags" do
visit project_protected_tags_path(project)
set_protected_tag_name('some-tag')
- set_allowed_to('create') if Gitlab.ee?
- click_on "Protect"
+ set_allowed_to('create')
+ click_on_protect
within(".protected-tags-list") { expect(page).to have_content('some-tag') }
expect(ProtectedTag.count).to eq(1)
@@ -30,8 +30,8 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
visit project_protected_tags_path(project)
set_protected_tag_name('some-tag')
- set_allowed_to('create') if Gitlab.ee?
- click_on "Protect"
+ set_allowed_to('create')
+ click_on_protect
within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) }
end
@@ -39,8 +39,8 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
it "displays an error message if the named tag does not exist" do
visit project_protected_tags_path(project)
set_protected_tag_name('some-tag')
- set_allowed_to('create') if Gitlab.ee?
- click_on "Protect"
+ set_allowed_to('create')
+ click_on_protect
within(".protected-tags-list") { expect(page).to have_content('tag was removed') }
end
@@ -50,8 +50,8 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
it "allows creating protected tags with a wildcard" do
visit project_protected_tags_path(project)
set_protected_tag_name('*-stable')
- set_allowed_to('create') if Gitlab.ee?
- click_on "Protect"
+ set_allowed_to('create')
+ click_on_protect
within(".protected-tags-list") { expect(page).to have_content('*-stable') }
expect(ProtectedTag.count).to eq(1)
@@ -64,8 +64,8 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
visit project_protected_tags_path(project)
set_protected_tag_name('*-stable')
- set_allowed_to('create') if Gitlab.ee?
- click_on "Protect"
+ set_allowed_to('create')
+ click_on_protect
within(".protected-tags-list") do
expect(page).to have_content("Protected tags (2)")
@@ -80,8 +80,8 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
visit project_protected_tags_path(project)
set_protected_tag_name('*-stable')
- set_allowed_to('create') if Gitlab.ee?
- click_on "Protect"
+ set_allowed_to('create')
+ click_on_protect
visit project_protected_tags_path(project)
click_on "2 matching tags"
@@ -101,4 +101,14 @@ RSpec.describe 'Protected Tags', :js, :with_license, feature_category: :source_c
include_examples "protected tags > access control > CE"
end
+
+ context 'when the users for protected tags feature is off' do
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
+
+ include_examples 'Deploy keys with protected tags' do
+ let(:all_dropdown_sections) { ['Roles', 'Deploy Keys'] }
+ end
+ 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 334a192bec4..127176da3fb 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -26,11 +26,21 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
wait_for_all_requests
end
- it 'starts searching by pressing the enter key' do
- submit_search('gitlab')
+ context 'when searching by pressing the enter key' do
+ before do
+ submit_search('gitlab')
+ end
+
+ it 'renders page title' do
+ page.within('.page-title') do
+ expect(page).to have_content('Search')
+ end
+ end
- page.within('.page-title') do
- expect(page).to have_content('Search')
+ it 'renders breadcrumbs' do
+ page.within('.breadcrumbs-links') do
+ expect(page).to have_content('Search')
+ end
end
end
diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb
index de81444ed71..d162b24175f 100644
--- a/spec/features/security/admin_access_spec.rb
+++ b/spec/features/security/admin_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Admin::Projects", feature_category: :permissions do
+RSpec.describe "Admin::Projects", feature_category: :system_access do
include AccessMatchers
describe "GET /admin/projects" do
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 948a4567624..0d60f1b1d11 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Dashboard access", feature_category: :permissions do
+RSpec.describe "Dashboard access", feature_category: :system_access do
include AccessMatchers
describe "GET /dashboard" do
diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb
index ad2df4a1882..49f81600ac2 100644
--- a/spec/features/security/group/internal_access_spec.rb
+++ b/spec/features/security/group/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Internal Group access', feature_category: :permissions do
+RSpec.describe 'Internal Group access', feature_category: :system_access do
include AccessMatchers
let(:group) { create(:group, :internal) }
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 2e7b7512b45..5206667427e 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Private Group access', feature_category: :permissions do
+RSpec.describe 'Private Group access', feature_category: :system_access do
include AccessMatchers
let(:group) { create(:group, :private) }
diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb
index 513c5710c8f..5c5580908aa 100644
--- a/spec/features/security/group/public_access_spec.rb
+++ b/spec/features/security/group/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Public Group access', feature_category: :permissions do
+RSpec.describe 'Public Group access', feature_category: :system_access do
include AccessMatchers
let(:group) { create(:group, :public) }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index e35e7ed742b..8ad4bedfdf8 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Internal Project Access", feature_category: :permissions do
+RSpec.describe "Internal Project Access", feature_category: :system_access do
include AccessMatchers
let_it_be(:project, reload: true) { create(:project, :internal, :repository, :with_namespace_settings) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 59ddb18ae8a..d2d74ecf5c9 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Private Project Access", feature_category: :permissions do
+RSpec.describe "Private Project Access", feature_category: :system_access do
include AccessMatchers
let_it_be(:project, reload: true) do
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 425691001f2..916f289b0b8 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Public Project Access", feature_category: :permissions do
+RSpec.describe "Public Project Access", feature_category: :system_access do
include AccessMatchers
let_it_be(:project, reload: true) do
diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb
index b7dcc5f31d3..6ed0ec20210 100644
--- a/spec/features/security/project/snippet/internal_access_spec.rb
+++ b/spec/features/security/project/snippet/internal_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Internal Project Snippets Access", feature_category: :permissions do
+RSpec.describe "Internal Project Snippets Access", feature_category: :system_access do
include AccessMatchers
let_it_be(:project) { create(:project, :internal) }
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index 0ae45abb7ec..ef61f79a1b5 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Private Project Snippets Access", feature_category: :permissions do
+RSpec.describe "Private Project Snippets Access", feature_category: :system_access do
include AccessMatchers
let_it_be(:project) { create(:project, :private) }
diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb
index b98f665c0dc..27fee745635 100644
--- a/spec/features/security/project/snippet/public_access_spec.rb
+++ b/spec/features/security/project/snippet/public_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe "Public Project Snippets Access", feature_category: :permissions do
+RSpec.describe "Public Project Snippets Access", feature_category: :system_access do
include AccessMatchers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 5d9b451cdf6..541f94a9340 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
end
end
- it "verified and the gpg user's profile doesn't exist anymore" do
+ it "verified and the gpg user's profile doesn't exist anymore", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/395802' do
user_1_key
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index dc2fcdd7305..d6ff8c066c4 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -28,11 +28,10 @@ RSpec.describe 'Snippet', :js, feature_category: :source_code_management do
it_behaves_like 'a dashboard page with sidebar', :dashboard_snippets_path, :snippets
context 'when unauthenticated' do
- it 'does not have the sidebar' do
+ it 'shows the "Explore" sidebar' do
visit snippet_path(snippet)
- expect(page).to have_title _('Snippets')
- expect(page).not_to have_css('aside.nav-sidebar')
+ expect(page).to have_css('aside.nav-sidebar[aria-label="Explore"]')
end
end
diff --git a/spec/features/topic_show_spec.rb b/spec/features/topic_show_spec.rb
index d640e4e4edb..39b8782ea58 100644
--- a/spec/features/topic_show_spec.rb
+++ b/spec/features/topic_show_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Topic show page', feature_category: :projects do
it 'shows title, avatar and description as markdown' do
expect(page).to have_content(topic.title)
expect(page).not_to have_content(topic.name)
- expect(page).to have_selector('.avatar-container > img.topic-avatar')
+ expect(page).to have_selector('.gl-avatar.gl-avatar-s64')
expect(find('.topic-description')).to have_selector('p > strong')
expect(find('.topic-description')).to have_selector('p > a[rel]')
expect(find('.topic-description')).to have_selector('p > gl-emoji')
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
deleted file mode 100644
index 9ef0626b2b2..00000000000
--- a/spec/features/u2f_spec.rb
+++ /dev/null
@@ -1,216 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js,
-feature_category: :authentication_and_authorization do
- include Spec::Support::Helpers::Features::TwoFactorHelpers
-
- before do
- stub_feature_flags(webauthn: false)
- end
-
- it_behaves_like 'hardware device for 2fa', 'U2F'
-
- describe "registration" do
- let(:user) { create(:user) }
-
- before do
- gitlab_sign_in(user)
- user.update_attribute(:otp_required_for_login, true)
- end
-
- describe 'when 2FA via OTP is enabled' do
- it 'allows registering more than one device' do
- visit profile_account_path
-
- # First device
- manage_two_factor_authentication
- first_device = register_u2f_device
- expect(page).to have_content('Your U2F device was registered')
-
- # Second device
- second_device = register_u2f_device(name: 'My other device')
- expect(page).to have_content('Your U2F device was registered')
-
- expect(page).to have_content(first_device.name)
- expect(page).to have_content(second_device.name)
- expect(U2fRegistration.count).to eq(2)
- end
- end
-
- it 'allows the same device to be registered for multiple users' do
- # U2f specs will be removed after WebAuthn migration completed
- pending('FakeU2fDevice has static key handle, '\
- 'leading to duplicate credential_xid for WebAuthn during migration, '\
- 'resulting in unique constraint violation')
-
- # First user
- visit profile_account_path
- manage_two_factor_authentication
- u2f_device = register_u2f_device
- expect(page).to have_content('Your U2F device was registered')
- gitlab_sign_out
-
- # Second user
- user = gitlab_sign_in(:user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_u2f_device(u2f_device, name: 'My other device')
- expect(page).to have_content('Your U2F device was registered')
-
- expect(U2fRegistration.count).to eq(2)
- end
-
- context "when there are form errors" do
- it "doesn't register the device if there are errors" do
- visit profile_account_path
- manage_two_factor_authentication
-
- # Have the "u2f device" respond with bad data
- page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- click_on 'Register device'
-
- expect(U2fRegistration.count).to eq(0)
- expect(page).to have_content("The form contains the following error")
- expect(page).to have_content("did not send a valid JSON response")
- end
-
- it "allows retrying registration" do
- visit profile_account_path
- manage_two_factor_authentication
-
- # Failed registration
- page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- click_on 'Register device'
- expect(page).to have_content("The form contains the following error")
-
- # Successful registration
- register_u2f_device
-
- expect(page).to have_content('Your U2F device was registered')
- expect(U2fRegistration.count).to eq(1)
- end
- end
- end
-
- describe "authentication" do
- let(:user) { create(:user) }
-
- before do
- # Register and logout
- gitlab_sign_in(user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- @u2f_device = register_u2f_device
- gitlab_sign_out
- end
-
- describe "when 2FA via OTP is disabled" do
- it "allows logging in with the U2F device" do
- user.update_attribute(:otp_required_for_login, false)
- gitlab_sign_in(user)
-
- @u2f_device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
- end
-
- describe "when 2FA via OTP is enabled" do
- it "allows logging in with the U2F device" do
- user.update_attribute(:otp_required_for_login, true)
- gitlab_sign_in(user)
-
- @u2f_device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
- end
-
- describe "when a given U2F device has already been registered by another user" do
- describe "but not the current user" do
- it "does not allow logging in with that particular device" do
- # Register current user with the different U2F device
- current_user = gitlab_sign_in(:user)
- current_user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_u2f_device(name: 'My other device')
- gitlab_sign_out
-
- # Try authenticating user with the old U2F device
- gitlab_sign_in(current_user)
- @u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('Authentication via U2F device failed')
- end
- end
-
- describe "and also the current user" do
- it "allows logging in with that particular device" do
- # U2f specs will be removed after WebAuthn migration completed
- pending('FakeU2fDevice has static key handle, '\
- 'leading to duplicate credential_xid for WebAuthn during migration, '\
- 'resulting in unique constraint violation')
-
- # Register current user with the same U2F device
- current_user = gitlab_sign_in(:user)
- current_user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_u2f_device(@u2f_device)
- gitlab_sign_out
-
- # Try authenticating user with the same U2F device
- gitlab_sign_in(current_user)
- @u2f_device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
- end
- end
-
- describe "when a given U2F device has not been registered" do
- it "does not allow logging in with that particular device" do
- unregistered_device = FakeU2fDevice.new(page, 'My device')
- gitlab_sign_in(user)
- unregistered_device.respond_to_u2f_authentication
-
- expect(page).to have_content('Authentication via U2F device failed')
- end
- end
-
- describe "when more than one device has been registered by the same user" do
- it "allows logging in with either device" do
- # Register first device
- user = gitlab_sign_in(:user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_two_factor_auth_path
- expect(page).to have_content("Your device needs to be set up.")
- first_device = register_u2f_device
-
- # Register second device
- visit profile_two_factor_auth_path
- expect(page).to have_content("Your device needs to be set up.")
- second_device = register_u2f_device(name: 'My other device')
- gitlab_sign_out
-
- # Authenticate as both devices
- [first_device, second_device].each do |device|
- gitlab_sign_in(user)
- device.respond_to_u2f_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
-
- gitlab_sign_out
- end
- end
- end
- end
-end
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index 23fa6261bd5..28699bc2c24 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :not_owned do
+RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :shared do
include Warden::Test::Helpers
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 5e683befeec..37b5d80ed61 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -109,6 +109,10 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'within the grace period' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
+ end
+
it 'allows to login' do
expect(authentication_metrics).to increment(:user_authenticated_counter)
@@ -137,11 +141,9 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
context 'when resending the confirmation email' do
- it 'redirects to the "almost there" page' do
- stub_feature_flags(soft_email_confirmation: false)
-
- user = create(:user)
+ let_it_be(:user) { create(:user) }
+ it 'redirects to the "almost there" page' do
visit new_user_confirmation_path
fill_in 'user_email', with: user.email
click_button 'Resend'
@@ -207,8 +209,89 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
describe 'with two-factor authentication', :js do
def enter_code(code)
- fill_in 'user_otp_attempt', with: code
- click_button 'Verify code'
+ if page.has_content?("Sign in via 2FA code")
+ click_on("Sign in via 2FA code")
+ enter_code(code)
+ else
+ fill_in 'user_otp_attempt', with: code
+ click_button 'Verify code'
+ end
+ end
+
+ shared_examples_for 'can login with recovery codes' do
+ context 'using backup code' do
+ let(:codes) { user.generate_otp_backup_codes! }
+
+ before do
+ expect(codes.size).to eq 10
+
+ # Ensure the generated codes get saved
+ user.save!(touch: false)
+ end
+
+ context 'with valid code' do
+ it 'allows login' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_two_factor_authenticated_counter)
+
+ enter_code(codes.sample)
+
+ expect(page).to have_current_path root_path, ignore_query: true
+ end
+
+ it 'invalidates the used code' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_two_factor_authenticated_counter)
+
+ expect { enter_code(codes.sample) }
+ .to change { user.reload.otp_backup_codes.size }.by(-1)
+ end
+
+ it 'invalidates backup codes twice in a row' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter).twice
+ .and increment(:user_two_factor_authenticated_counter).twice
+ .and increment(:user_session_destroyed_counter)
+
+ random_code = codes.delete(codes.sample)
+ expect { enter_code(random_code) }
+ .to change { user.reload.otp_backup_codes.size }.by(-1)
+
+ gitlab_sign_out
+ gitlab_sign_in(user)
+
+ expect { enter_code(codes.sample) }
+ .to change { user.reload.otp_backup_codes.size }.by(-1)
+ end
+
+ it 'triggers ActiveSession.cleanup for the user' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_two_factor_authenticated_counter)
+ expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
+
+ enter_code(codes.sample)
+ end
+ end
+
+ context 'with invalid code' do
+ it 'blocks login' do
+ # TODO, invalid two factor authentication does not increment
+ # metrics / counters, see gitlab-org/gitlab-ce#49785
+
+ code = codes.sample
+ expect(user.invalidate_otp_backup_code!(code)).to eq true
+
+ user.save!(touch: false)
+ expect(user.reload.otp_backup_codes.size).to eq 9
+
+ enter_code(code)
+ expect(page).to have_content('Invalid two-factor code.')
+ end
+ end
+ end
end
context 'with valid username/password' do
@@ -216,8 +299,6 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
before do
gitlab_sign_in(user, remember: true)
-
- expect(page).to have_content('Two-factor authentication code')
end
it 'does not show a "You are already signed in." error message' do
@@ -290,78 +371,16 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
end
- context 'using backup code' do
- let(:codes) { user.generate_otp_backup_codes! }
-
- before do
- expect(codes.size).to eq 10
-
- # Ensure the generated codes get saved
- user.save!(touch: false)
- end
-
- context 'with valid code' do
- it 'allows login' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_two_factor_authenticated_counter)
-
- enter_code(codes.sample)
-
- expect(page).to have_current_path root_path, ignore_query: true
- end
+ context 'when user with TOTP enabled' do
+ let(:user) { create(:user, :two_factor) }
- it 'invalidates the used code' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_two_factor_authenticated_counter)
-
- expect { enter_code(codes.sample) }
- .to change { user.reload.otp_backup_codes.size }.by(-1)
- end
-
- it 'invalidates backup codes twice in a row' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter).twice
- .and increment(:user_two_factor_authenticated_counter).twice
- .and increment(:user_session_destroyed_counter)
-
- random_code = codes.delete(codes.sample)
- expect { enter_code(random_code) }
- .to change { user.reload.otp_backup_codes.size }.by(-1)
-
- gitlab_sign_out
- gitlab_sign_in(user)
-
- expect { enter_code(codes.sample) }
- .to change { user.reload.otp_backup_codes.size }.by(-1)
- end
-
- it 'triggers ActiveSession.cleanup for the user' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_two_factor_authenticated_counter)
- expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
-
- enter_code(codes.sample)
- end
- end
-
- context 'with invalid code' do
- it 'blocks login' do
- # TODO, invalid two factor authentication does not increment
- # metrics / counters, see gitlab-org/gitlab-ce#49785
-
- code = codes.sample
- expect(user.invalidate_otp_backup_code!(code)).to eq true
+ include_examples 'can login with recovery codes'
+ end
- user.save!(touch: false)
- expect(user.reload.otp_backup_codes.size).to eq 9
+ context 'when user with only Webauthn enabled' do
+ let(:user) { create(:user, :two_factor_via_webauthn, registrations_count: 1) }
- enter_code(code)
- expect(page).to have_content('Invalid two-factor code.')
- end
- end
+ include_examples 'can login with recovery codes'
end
end
@@ -379,8 +398,8 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
context 'when authn_context is worth two factors' do
let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml')
- .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
end
it 'signs user in without prompting for second factor' do
@@ -394,7 +413,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
sign_in_using_saml!
expect_single_session_with_authenticated_ttl
- expect(page).not_to have_content('Two-Factor Authentication')
+ expect(page).not_to have_content(_('Enter verification code'))
expect(page).to have_current_path root_path, ignore_query: true
end
end
@@ -408,7 +427,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
sign_in_using_saml!
- expect(page).to have_content('Two-factor authentication code')
+ expect(page).to have_content('Enter verification code')
enter_code(user.current_otp)
@@ -928,22 +947,22 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
it 'asks the user to accept the terms before setting an email',
quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/388049', type: :flaky } do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
- gitlab_sign_in_via('saml', user, 'my-uid')
+ gitlab_sign_in_via('saml', user, 'my-uid')
- expect_to_be_on_terms_page
- click_button 'Accept terms'
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
- expect(page).to have_current_path(profile_path, ignore_query: true)
+ expect(page).to have_current_path(profile_path, ignore_query: true)
- fill_in 'Email', with: 'hello@world.com'
+ fill_in 'Email', with: 'hello@world.com'
- click_button 'Update profile settings'
+ click_button 'Update profile settings'
- expect(page).to have_content('Profile was successfully updated')
- end
+ expect(page).to have_content('Profile was successfully updated')
+ end
end
end
@@ -954,8 +973,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
before do
- stub_application_setting_enum('email_confirmation_setting', 'hard')
- stub_feature_flags(soft_email_confirmation: true)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
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 88b2d918976..9aef3ed7cd6 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -149,7 +149,7 @@ RSpec.describe 'User page', feature_category: :user_profile do
end
end
- context 'follow/unfollow and followers/following' do
+ context 'follow/unfollow and followers/following', :js do
let_it_be(:followee) { create(:user) }
let_it_be(:follower) { create(:user) }
@@ -159,21 +159,33 @@ RSpec.describe 'User page', feature_category: :user_profile do
expect(page).not_to have_button(text: 'Follow', class: 'gl-button')
end
- it 'shows 0 followers and 0 following' do
- subject
+ shared_examples 'follower tabs with count badges' do
+ it 'shows 0 followers and 0 following' do
+ subject
+
+ expect(page).to have_content('Followers 0')
+ expect(page).to have_content('Following 0')
+ end
+
+ it 'shows 1 followers and 1 following' do
+ follower.follow(user)
+ user.follow(followee)
- expect(page).to have_content('0 followers')
- expect(page).to have_content('0 following')
+ subject
+
+ expect(page).to have_content('Followers 1')
+ expect(page).to have_content('Following 1')
+ end
end
- it 'shows 1 followers and 1 following' do
- follower.follow(user)
- user.follow(followee)
+ it_behaves_like 'follower tabs with count badges'
- subject
+ context 'with profile_tabs_vue feature flag disabled' do
+ before_all do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
- expect(page).to have_content('1 follower')
- expect(page).to have_content('1 following')
+ it_behaves_like 'follower tabs with count badges'
end
it 'does show button to follow' do
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 11ff318c346..a762198d3c3 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -200,9 +200,8 @@ RSpec.describe 'Signup', feature_category: :user_profile do
stub_application_setting_enum('email_confirmation_setting', 'hard')
end
- context 'when soft email confirmation is not enabled' do
+ context 'when email confirmation setting is not `soft`' do
before do
- stub_feature_flags(soft_email_confirmation: false)
stub_feature_flags(identity_verification: false)
end
@@ -221,9 +220,9 @@ RSpec.describe 'Signup', feature_category: :user_profile do
end
end
- context 'when soft email confirmation is enabled' do
+ context 'when email confirmation setting is `soft`' do
before do
- stub_feature_flags(soft_email_confirmation: true)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
end
it 'creates the user account and sends a confirmation email' do
@@ -384,7 +383,7 @@ RSpec.describe 'Signup', feature_category: :user_profile do
expect(page.body).not_to match(/#{new_user.password}/)
end
- context 'with invalid email', :saas, :js do
+ context 'with invalid email', :js do
it_behaves_like 'user email validation' do
let(:path) { new_user_registration_path }
end
diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb
index 859793d1353..fbbc746c0b0 100644
--- a/spec/features/webauthn_spec.rb
+++ b/spec/features/webauthn_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_category: :authentication_and_authorization do
+RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_category: :system_access do
include Spec::Support::Helpers::Features::TwoFactorHelpers
let(:app_id) { "http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}" }
@@ -10,6 +10,113 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
WebAuthn.configuration.origin = app_id
end
+ context 'when the webauth_without_totp feature flag is enabled' do
+ # Some of the shared tests don't apply. After removing U2F support and the `webauthn_without_totp` feature flag, refactor the shared tests.
+ # TODO: it_behaves_like 'hardware device for 2fa', 'WebAuthn'
+
+ describe 'registration' do
+ let(:user) { create(:user) }
+
+ before do
+ gitlab_sign_in(user)
+ end
+
+ it 'shows an error when using a wrong password' do
+ visit profile_account_path
+
+ # First device
+ enable_two_factor_authentication
+ webauthn_device_registration(password: 'fake')
+ expect(page).to have_content(_('You must provide a valid current password.'))
+ end
+
+ it 'allows registering more than one device' do
+ visit profile_account_path
+
+ # First device
+ enable_two_factor_authentication
+ first_device = webauthn_device_registration(password: user.password)
+ expect(page).to have_content('Your WebAuthn device was registered!')
+ copy_recovery_codes
+ manage_two_factor_authentication
+
+ # Second device
+ second_device = webauthn_device_registration(name: 'My other device', password: user.password)
+ expect(page).to have_content('Your WebAuthn device was registered!')
+
+ expect(page).to have_content(first_device.name)
+ expect(page).to have_content(second_device.name)
+ expect(WebauthnRegistration.count).to eq(2)
+ end
+
+ it 'allows the same device to be registered for multiple users' do
+ # First user
+ visit profile_account_path
+ enable_two_factor_authentication
+ webauthn_device = webauthn_device_registration(password: user.password)
+ expect(page).to have_content('Your WebAuthn device was registered!')
+ gitlab_sign_out
+
+ # Second user
+ user = gitlab_sign_in(:user)
+ visit profile_account_path
+ enable_two_factor_authentication
+ webauthn_device_registration(webauthn_device: webauthn_device, name: 'My other device', password: user.password)
+ expect(page).to have_content('Your WebAuthn device was registered!')
+
+ expect(WebauthnRegistration.count).to eq(2)
+ end
+
+ context 'when there are form errors' do
+ let(:mock_register_js) do
+ <<~JS
+ const mockResponse = {
+ type: 'public-key',
+ id: '',
+ rawId: '',
+ response: {
+ clientDataJSON: '',
+ attestationObject: '',
+ },
+ getClientExtensionResults: () => {},
+ };
+ navigator.credentials.create = () => Promise.resolve(mockResponse);
+ JS
+ end
+
+ it "doesn't register the device if there are errors" do
+ visit profile_account_path
+ enable_two_factor_authentication
+
+ # Have the "webauthn device" respond with bad data
+ page.execute_script(mock_register_js)
+ click_on _('Set up new device')
+ webauthn_fill_form_and_submit(password: user.password)
+ expect(page).to have_content(_('Your WebAuthn device did not send a valid JSON response.'))
+
+ expect(WebauthnRegistration.count).to eq(0)
+ end
+
+ it 'allows retrying registration' do
+ visit profile_account_path
+ enable_two_factor_authentication
+
+ # Failed registration
+ page.execute_script(mock_register_js)
+ click_on _('Set up new device')
+ webauthn_fill_form_and_submit(password: user.password)
+ expect(page).to have_content(_('Your WebAuthn device did not send a valid JSON response.'))
+
+ # Successful registration
+ webauthn_device_registration(password: user.password)
+
+ expect(page).to have_content('Your WebAuthn device was registered!')
+ expect(WebauthnRegistration.count).to eq(1)
+ end
+ end
+ end
+ end
+
context 'when the webauth_without_totp feature flag is disabled' do
before do
stub_feature_flags(webauthn_without_totp: false)
@@ -114,99 +221,99 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
end
end
end
+ end
- describe 'authentication' do
- let(:otp_required_for_login) { true }
- let(:user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
- let!(:webauthn_device) do
- add_webauthn_device(app_id, user)
- end
+ describe 'authentication' do
+ let(:otp_required_for_login) { true }
+ let(:user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ let!(:webauthn_device) do
+ add_webauthn_device(app_id, user)
+ end
- describe 'when 2FA via OTP is disabled' do
- let(:otp_required_for_login) { false }
+ describe 'when 2FA via OTP is disabled' do
+ let(:otp_required_for_login) { false }
- it 'allows logging in with the WebAuthn device' do
- gitlab_sign_in(user)
+ it 'allows logging in with the WebAuthn device' do
+ gitlab_sign_in(user)
- webauthn_device.respond_to_webauthn_authentication
+ webauthn_device.respond_to_webauthn_authentication
- expect(page).to have_css('.sign-out-link', visible: false)
- end
+ expect(page).to have_css('.sign-out-link', visible: false)
end
+ end
- describe 'when 2FA via OTP is enabled' do
- it 'allows logging in with the WebAuthn device' do
- gitlab_sign_in(user)
+ describe 'when 2FA via OTP is enabled' do
+ it 'allows logging in with the WebAuthn device' do
+ gitlab_sign_in(user)
- webauthn_device.respond_to_webauthn_authentication
+ webauthn_device.respond_to_webauthn_authentication
- expect(page).to have_css('.sign-out-link', visible: false)
- end
+ expect(page).to have_css('.sign-out-link', visible: false)
end
+ end
- describe 'when a given WebAuthn device has already been registered by another user' do
- describe 'but not the current user' do
- let(:other_user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ describe 'when a given WebAuthn device has already been registered by another user' do
+ describe 'but not the current user' do
+ let(:other_user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
- it 'does not allow logging in with that particular device' do
- # Register other user with a different WebAuthn device
- other_device = add_webauthn_device(app_id, other_user)
+ it 'does not allow logging in with that particular device' do
+ # Register other user with a different WebAuthn device
+ other_device = add_webauthn_device(app_id, other_user)
- # Try authenticating user with the old WebAuthn device
- gitlab_sign_in(user)
- other_device.respond_to_webauthn_authentication
- expect(page).to have_content('Authentication via WebAuthn device failed')
- end
+ # Try authenticating user with the old WebAuthn device
+ gitlab_sign_in(user)
+ other_device.respond_to_webauthn_authentication
+ expect(page).to have_content('Authentication via WebAuthn device failed')
end
+ end
+
+ describe "and also the current user" do
+ # TODO Uncomment once WebAuthn::FakeClient supports passing credential options
+ # (especially allow_credentials, as this is needed to specify which credential the
+ # fake client should use. Currently, the first credential is always used).
+ # There is an issue open for this: https://github.com/cedarcode/webauthn-ruby/issues/259
+ it "allows logging in with that particular device" do
+ pending("support for passing credential options in FakeClient")
+ # Register current user with the same WebAuthn device
+ current_user = gitlab_sign_in(:user)
+ visit profile_account_path
+ manage_two_factor_authentication
+ register_webauthn_device(webauthn_device)
+ gitlab_sign_out
+
+ # Try authenticating user with the same WebAuthn device
+ gitlab_sign_in(current_user)
+ webauthn_device.respond_to_webauthn_authentication
- describe "and also the current user" do
- # TODO Uncomment once WebAuthn::FakeClient supports passing credential options
- # (especially allow_credentials, as this is needed to specify which credential the
- # fake client should use. Currently, the first credential is always used).
- # There is an issue open for this: https://github.com/cedarcode/webauthn-ruby/issues/259
- it "allows logging in with that particular device" do
- pending("support for passing credential options in FakeClient")
- # Register current user with the same WebAuthn device
- current_user = gitlab_sign_in(:user)
- visit profile_account_path
- manage_two_factor_authentication
- register_webauthn_device(webauthn_device)
- gitlab_sign_out
-
- # Try authenticating user with the same WebAuthn device
- gitlab_sign_in(current_user)
- webauthn_device.respond_to_webauthn_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
- end
+ expect(page).to have_css('.sign-out-link', visible: false)
end
end
+ end
- describe 'when a given WebAuthn device has not been registered' do
- it 'does not allow logging in with that particular device' do
- unregistered_device = FakeWebauthnDevice.new(page, 'My device')
- gitlab_sign_in(user)
- unregistered_device.respond_to_webauthn_authentication
+ describe 'when a given WebAuthn device has not been registered' do
+ it 'does not allow logging in with that particular device' do
+ unregistered_device = FakeWebauthnDevice.new(page, 'My device')
+ gitlab_sign_in(user)
+ unregistered_device.respond_to_webauthn_authentication
- expect(page).to have_content('Authentication via WebAuthn device failed')
- end
+ expect(page).to have_content('Authentication via WebAuthn device failed')
end
+ end
- describe 'when more than one device has been registered by the same user' do
- it 'allows logging in with either device' do
- first_device = add_webauthn_device(app_id, user)
- second_device = add_webauthn_device(app_id, user)
+ describe 'when more than one device has been registered by the same user' do
+ it 'allows logging in with either device' do
+ first_device = add_webauthn_device(app_id, user)
+ second_device = add_webauthn_device(app_id, user)
- # Authenticate as both devices
- [first_device, second_device].each do |device|
- gitlab_sign_in(user)
- # register_webauthn_device(device)
- device.respond_to_webauthn_authentication
+ # Authenticate as both devices
+ [first_device, second_device].each do |device|
+ gitlab_sign_in(user)
+ # register_webauthn_device(device)
+ device.respond_to_webauthn_authentication
- expect(page).to have_css('.sign-out-link', visible: false)
+ expect(page).to have_css('.sign-out-link', visible: false)
- gitlab_sign_out
- end
+ gitlab_sign_out
end
end
end
diff --git a/spec/features/whats_new_spec.rb b/spec/features/whats_new_spec.rb
index 6b19ab28b44..3668d90f2e9 100644
--- a/spec/features/whats_new_spec.rb
+++ b/spec/features/whats_new_spec.rb
@@ -2,13 +2,11 @@
require "spec_helper"
-RSpec.describe "renders a `whats new` dropdown item", feature_category: :not_owned do
+RSpec.describe "renders a `whats new` dropdown item", feature_category: :onboarding do
let_it_be(:user) { create(:user) }
context 'when not logged in' do
- it 'and on .com it renders' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
+ it 'and on SaaS it renders', :saas do
visit user_path(user)
page.within '.header-help' do
diff --git a/spec/features/work_items/work_item_spec.rb b/spec/features/work_items/work_item_spec.rb
deleted file mode 100644
index 3c71a27ff82..00000000000
--- a/spec/features/work_items/work_item_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Work item', :js, feature_category: :team_planning do
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:user) { create(:user) }
- let_it_be(:work_item) { create(:work_item, project: project) }
-
- context 'for signed in user' do
- before do
- project.add_developer(user)
-
- sign_in(user)
-
- visit project_work_items_path(project, work_items_path: work_item.id)
- end
-
- it_behaves_like 'work items status'
- it_behaves_like 'work items assignees'
- it_behaves_like 'work items labels'
- it_behaves_like 'work items comments'
- it_behaves_like 'work items description'
- end
-
- context 'for signed in owner' do
- before do
- project.add_owner(user)
-
- sign_in(user)
-
- visit project_work_items_path(project, work_items_path: work_item.id)
- end
-
- it_behaves_like 'work items invite members'
- end
-end
diff --git a/spec/finders/abuse_reports_finder_spec.rb b/spec/finders/abuse_reports_finder_spec.rb
index 52620b3e421..d3b148375d4 100644
--- a/spec/finders/abuse_reports_finder_spec.rb
+++ b/spec/finders/abuse_reports_finder_spec.rb
@@ -3,25 +3,124 @@
require 'spec_helper'
RSpec.describe AbuseReportsFinder, '#execute' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:abuse_report_1) { create(:abuse_report, id: 20, category: 'spam', user: user1) }
+ let_it_be(:abuse_report_2) do
+ create(:abuse_report, :closed, id: 30, category: 'phishing', user: user2, reporter: reporter)
+ end
+
let(:params) { {} }
- let!(:user1) { create(:user) }
- let!(:user2) { create(:user) }
- let!(:abuse_report_1) { create(:abuse_report, user: user1) }
- let!(:abuse_report_2) { create(:abuse_report, user: user2) }
subject { described_class.new(params).execute }
- context 'empty params' do
+ context 'when params is empty' do
it 'returns all abuse reports' do
expect(subject).to match_array([abuse_report_1, abuse_report_2])
end
end
- context 'params[:user_id] is present' do
+ context 'when params[:user_id] is present' do
let(:params) { { user_id: user2 } }
it 'returns abuse reports for the specified user' do
expect(subject).to match_array([abuse_report_2])
end
end
+
+ shared_examples 'returns filtered reports' do |filter_field|
+ it "returns abuse reports filtered by #{filter_field}_id" do
+ expect(subject).to match_array(filtered_reports)
+ end
+
+ context "when no user has username = params[:#{filter_field}]" do
+ before do
+ allow(User).to receive_message_chain(:by_username, :pick)
+ .with(params[filter_field])
+ .with(:id)
+ .and_return(nil)
+ end
+
+ it 'returns all abuse reports' do
+ expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ end
+ end
+ end
+
+ context 'when params[:user] is present' do
+ it_behaves_like 'returns filtered reports', :user do
+ let(:params) { { user: user1.username } }
+ let(:filtered_reports) { [abuse_report_1] }
+ end
+ end
+
+ context 'when params[:reporter] is present' do
+ it_behaves_like 'returns filtered reports', :reporter do
+ let(:params) { { reporter: reporter.username } }
+ let(:filtered_reports) { [abuse_report_2] }
+ end
+ end
+
+ context 'when params[:status] is present' do
+ context 'when value is "open"' do
+ let(:params) { { status: 'open' } }
+
+ it 'returns only open abuse reports' do
+ expect(subject).to match_array([abuse_report_1])
+ end
+ end
+
+ context 'when value is "closed"' do
+ let(:params) { { status: 'closed' } }
+
+ it 'returns only closed abuse reports' do
+ expect(subject).to match_array([abuse_report_2])
+ end
+ end
+ end
+
+ context 'when params[:category] is present' do
+ let(:params) { { category: 'phishing' } }
+
+ it 'returns abuse reports with the specified category' do
+ expect(subject).to match_array([abuse_report_2])
+ end
+ end
+
+ describe 'sorting' do
+ let(:params) { { sort: 'created_at_asc' } }
+
+ it 'returns reports sorted by the specified sort attribute' do
+ expect(subject).to eq [abuse_report_1, abuse_report_2]
+ end
+
+ context 'when sort is not specified' do
+ let(:params) { {} }
+
+ it "returns reports sorted by #{described_class::DEFAULT_SORT}" do
+ expect(subject).to eq [abuse_report_2, abuse_report_1]
+ end
+ end
+
+ context 'when sort is not supported' do
+ let(:params) { { sort: 'superiority' } }
+
+ it "returns reports sorted by #{described_class::DEFAULT_SORT}" do
+ expect(subject).to eq [abuse_report_2, abuse_report_1]
+ end
+ end
+
+ context 'when abuse_reports_list feature flag is disabled' do
+ let_it_be(:abuse_report_3) { create(:abuse_report, id: 10) }
+
+ before do
+ stub_feature_flags(abuse_reports_list: false)
+ end
+
+ it 'returns reports sorted by id in descending order' do
+ expect(subject).to eq [abuse_report_2, abuse_report_1, abuse_report_3]
+ end
+ end
+ end
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 4a5eb389906..5d748f71816 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -225,4 +225,56 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do
end
end
end
+
+ context 'filter by user type' do
+ subject(:by_user_type) { described_class.new(group, user1, params: { user_type: user_type }).execute }
+
+ let_it_be(:service_account) { create(:user, :service_account) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+
+ let_it_be(:service_account_member) { group.add_developer(service_account) }
+ let_it_be(:project_bot_member) { group.add_developer(project_bot) }
+
+ context 'when the user is an owner' do
+ before do
+ group.add_owner(user1)
+ end
+
+ context 'when filtering by project bots' do
+ let(:user_type) { 'project_bot' }
+
+ it 'returns filtered members' do
+ expect(by_user_type).to match_array([project_bot_member])
+ end
+ end
+
+ context 'when filtering by service accounts' do
+ let(:user_type) { 'service_account' }
+
+ it 'returns filtered members' do
+ expect(by_user_type).to match_array([service_account_member])
+ end
+ end
+ end
+
+ context 'when the user is a maintainer' do
+ let(:user_type) { 'service_account' }
+
+ let_it_be(:user1_member) { group.add_maintainer(user1) }
+
+ it 'returns unfiltered members' do
+ expect(by_user_type).to match_array([user1_member, service_account_member, project_bot_member])
+ end
+ end
+
+ context 'when the user is a developer' do
+ let(:user_type) { 'service_account' }
+
+ let_it_be(:user1_member) { group.add_developer(user1) }
+
+ it 'returns unfiltered members' do
+ expect(by_user_type).to match_array([user1_member, service_account_member, project_bot_member])
+ end
+ end
+ end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index e8099924638..306acb9391d 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -493,6 +493,24 @@ RSpec.describe MergeRequestsFinder, feature_category: :code_review_workflow do
end
end
+ context 'filtering by approved' do
+ before do
+ create(:approval, merge_request: merge_request3, user: user2)
+ end
+
+ it 'for approved' do
+ merge_requests = described_class.new(user, { approved: true }).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+
+ it 'for not approved' do
+ merge_requests = described_class.new(user, { approved: false }).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request4, merge_request5)
+ end
+ end
+
context 'filtering by approved by username' do
let(:params) { { approved_by_usernames: user2.username } }
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index 8dd83df3a28..c4c62e21ad9 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -62,9 +62,31 @@ RSpec.describe MilestonesFinder do
end
context 'with filters' do
- let_it_be(:milestone_1) { create(:milestone, group: group, state: 'closed', title: 'one test', start_date: now - 1.day, due_date: now) }
- let_it_be(:milestone_3) { create(:milestone, project: project_1, state: 'closed', start_date: now + 2.days, due_date: now + 3.days) }
+ let_it_be(:milestone_1) do
+ create(
+ :milestone,
+ group: group,
+ state: 'closed',
+ title: 'one test',
+ start_date: now - 1.day,
+ due_date: now,
+ updated_at: now - 3.days
+ )
+ end
+
+ let_it_be(:milestone_3) do
+ create(
+ :milestone,
+ project: project_1,
+ state: 'closed',
+ description: 'three test',
+ start_date: now + 2.days,
+ due_date: now + 3.days,
+ updated_at: now - 5.days
+ )
+ end
+ let(:result) { described_class.new(params).execute }
let(:params) do
{
project_ids: [project_1.id, project_2.id],
@@ -76,62 +98,96 @@ RSpec.describe MilestonesFinder do
it 'filters by id' do
params[:ids] = [milestone_1.id, milestone_2.id]
- result = described_class.new(params).execute
-
expect(result).to contain_exactly(milestone_1, milestone_2)
end
it 'filters by active state' do
params[:state] = 'active'
- result = described_class.new(params).execute
expect(result).to contain_exactly(milestone_2, milestone_4)
end
it 'filters by closed state' do
params[:state] = 'closed'
- result = described_class.new(params).execute
expect(result).to contain_exactly(milestone_1, milestone_3)
end
it 'filters by title' do
- result = described_class.new(params.merge(title: 'one test')).execute
+ params[:title] = 'one test'
- expect(result.to_a).to contain_exactly(milestone_1)
+ expect(result).to contain_exactly(milestone_1)
end
it 'filters by search_title' do
- result = described_class.new(params.merge(search_title: 'one t')).execute
+ params[:search_title] = 'test'
+
+ expect(result).to contain_exactly(milestone_1)
+ end
+
+ it 'filters by search (title, description)' do
+ params[:search] = 'test'
- expect(result.to_a).to contain_exactly(milestone_1)
+ expect(result).to contain_exactly(milestone_1, milestone_3)
end
context 'by timeframe' do
it 'returns milestones with start_date and due_date between timeframe' do
params.merge!(start_date: now - 1.day, end_date: now + 3.days)
- milestones = described_class.new(params).execute
-
- expect(milestones).to match_array([milestone_1, milestone_2, milestone_3])
+ expect(result).to contain_exactly(milestone_1, milestone_2, milestone_3)
end
it 'returns milestones which starts before the timeframe' do
milestone = create(:milestone, project: project_2, start_date: now - 5.days)
params.merge!(start_date: now - 3.days, end_date: now - 2.days)
- milestones = described_class.new(params).execute
-
- expect(milestones).to match_array([milestone])
+ expect(result).to contain_exactly(milestone)
end
it 'returns milestones which ends after the timeframe' do
milestone = create(:milestone, project: project_2, due_date: now + 6.days)
params.merge!(start_date: now + 6.days, end_date: now + 7.days)
- milestones = described_class.new(params).execute
+ expect(result).to contain_exactly(milestone)
+ end
+ end
+
+ context 'by updated_at' do
+ it 'returns milestones updated before a given date' do
+ params[:updated_before] = 4.days.ago.iso8601
+
+ expect(result).to contain_exactly(milestone_3)
+ end
+
+ it 'returns milestones updated after a given date' do
+ params[:updated_after] = 4.days.ago.iso8601
+
+ expect(result).to contain_exactly(milestone_1, milestone_2, milestone_4)
+ end
+
+ it 'returns milestones updated between the given dates' do
+ params.merge!(updated_after: 6.days.ago.iso8601, updated_before: 4.days.ago.iso8601)
+
+ expect(result).to contain_exactly(milestone_3)
+ end
+ end
+
+ context 'by iids' do
+ before do
+ params[:iids] = 1
+ end
- expect(milestones).to match_array([milestone])
+ it 'returns milestone for the given iids' do
+ expect(result).to contain_exactly(milestone_2, milestone_3, milestone_4)
+ end
+
+ context 'when include_parent_milestones is true' do
+ it 'ignores the iid filter' do
+ params[:include_parent_milestones] = true
+
+ expect(result).to contain_exactly(milestone_1, milestone_2, milestone_3, milestone_4)
+ end
end
end
end
diff --git a/spec/finders/serverless_domain_finder_spec.rb b/spec/finders/serverless_domain_finder_spec.rb
deleted file mode 100644
index 4e6b9f07544..00000000000
--- a/spec/finders/serverless_domain_finder_spec.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ServerlessDomainFinder do
- let(:function_name) { 'test-function' }
- let(:pages_domain_name) { 'serverless.gitlab.io' }
- let(:valid_cluster_uuid) { 'aba1cdef123456f278' }
- let(:invalid_cluster_uuid) { 'aba1cdef123456f178' }
- let!(:environment) { create(:environment, name: 'test') }
-
- let(:pages_domain) do
- create(
- :pages_domain,
- :instance_serverless,
- domain: pages_domain_name
- )
- end
-
- let(:knative_with_ingress) do
- create(
- :clusters_applications_knative,
- external_ip: '10.0.0.1'
- )
- end
-
- let!(:serverless_domain_cluster) do
- create(
- :serverless_domain_cluster,
- uuid: 'abcdef12345678',
- pages_domain: pages_domain,
- knative: knative_with_ingress
- )
- end
-
- let(:valid_uri) { "https://#{function_name}-#{valid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
- let(:valid_fqdn) { "#{function_name}-#{valid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
- let(:invalid_uri) { "https://#{function_name}-#{invalid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
-
- let(:valid_finder) { described_class.new(valid_uri) }
- let(:invalid_finder) { described_class.new(invalid_uri) }
-
- describe '#serverless?' do
- context 'with a valid URI' do
- subject { valid_finder.serverless? }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with an invalid URI' do
- subject { invalid_finder.serverless? }
-
- it { is_expected.to be_falsy }
- end
- end
-
- describe '#serverless_domain_cluster_uuid' do
- context 'with a valid URI' do
- subject { valid_finder.serverless_domain_cluster_uuid }
-
- it { is_expected.to eq serverless_domain_cluster.uuid }
- end
-
- context 'with an invalid URI' do
- subject { invalid_finder.serverless_domain_cluster_uuid }
-
- it { is_expected.to be_nil }
- end
- end
-
- describe '#execute' do
- context 'with a valid URI' do
- let(:serverless_domain) do
- create(
- :serverless_domain,
- function_name: function_name,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- subject { valid_finder.execute }
-
- it 'has the correct function_name' do
- expect(subject.function_name).to eq function_name
- end
-
- it 'has the correct serverless_domain_cluster' do
- expect(subject.serverless_domain_cluster).to eq serverless_domain_cluster
- end
-
- it 'has the correct environment' do
- expect(subject.environment).to eq environment
- end
- end
-
- context 'with an invalid URI' do
- subject { invalid_finder.execute }
-
- it { is_expected.to be_nil }
- end
- end
-end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index efc609b3c3f..0ef4d6f82a9 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -1,8 +1,7 @@
{
"type": "object",
"required": [
- "status",
- "applications"
+ "status"
],
"properties": {
"status": {
@@ -10,12 +9,6 @@
},
"status_reason": {
"$ref": "types/nullable_string.json"
- },
- "applications": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/application_status"
- }
}
},
"additionalProperties": false,
@@ -115,4 +108,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/entities/discussion.json b/spec/fixtures/api/schemas/entities/discussion.json
index 45271926547..4af36b5814b 100644
--- a/spec/fixtures/api/schemas/entities/discussion.json
+++ b/spec/fixtures/api/schemas/entities/discussion.json
@@ -103,6 +103,9 @@
"noteable_type": {
"type": "string"
},
+ "project_id": {
+ "type": "integer"
+ },
"resolved": {
"type": "boolean"
},
@@ -207,4 +210,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/internal/pages/lookup_path.json b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
index 9d81ea495f1..8ca71870911 100644
--- a/spec/fixtures/api/schemas/internal/pages/lookup_path.json
+++ b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
@@ -23,7 +23,8 @@
},
"additionalProperties": false
},
- "prefix": { "type": "string" }
+ "prefix": { "type": "string" },
+ "unique_domain": { "type": ["string", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json
index 1987a0f2f71..60d6bb90b79 100644
--- a/spec/fixtures/api/schemas/public_api/v4/notes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/notes.json
@@ -78,6 +78,9 @@
"noteable_type": {
"type": "string"
},
+ "project_id": {
+ "type": "integer"
+ },
"resolved": {
"type": "boolean"
},
@@ -122,4 +125,4 @@
],
"additionalProperties": false
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/auth_key.p8 b/spec/fixtures/auth_key.p8
new file mode 100644
index 00000000000..1b53126536e
--- /dev/null
+++ b/spec/fixtures/auth_key.p8
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----
diff --git a/spec/fixtures/diagram.drawio.svg b/spec/fixtures/diagram.drawio.svg
new file mode 100644
index 00000000000..3eb6eb29921
--- /dev/null
+++ b/spec/fixtures/diagram.drawio.svg
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
+ width="177px" height="97px" viewBox="-0.5 -0.5 177 97"
+ content="&lt;mxfile host=&quot;embed.diagrams.net&quot; modified=&quot;2022-11-18T14:21:55.551Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36&quot; version=&quot;20.5.3&quot; etag=&quot;cTK3wL1ch5_8VL-J45NP&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;mWELjHy14aEMRdjyCi3_&quot; name=&quot;Page-1&quot;&gt;jZLBcoQgDIafhrvItPVcu+1eevLQMyOpMAPGYbFqn75Ygq7d2ZmeSL4kkPyEidrNb14O+h0VWFYWambihZUlr554PFayJFI9FAl03ihK2kFjvoFgThuNgsshMSDaYIYjbLHvoQ0HJr3H6Zj2ifb46iA7uAFNK+0t/TAq6D9TrPwMptP5ZV5QxMmcTOCipcLpCokTE7VHDMlycw12FS/rkupe70S3xjz04T8FZSr4knak2ZSRnZeO2gtLntnj2CtYywomnidtAjSDbNfoFH85Mh2cjR6PJt0KPsB8tzO+zRsXBdBB8EtMoQKRNaMdiSD50644fySmr9SuiEn65G67etchGiRFdnfJf2NXiytOPw==&lt;/diagram&gt;&lt;/mxfile&gt;"
+ style="background-color: rgb(255, 255, 255);">
+ <defs />
+ <g>
+ <rect x="8" y="8" width="160" height="80" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)"
+ pointer-events="all" />
+ <g transform="translate(-0.5 -0.5)">
+ <switch>
+ <foreignObject pointer-events="none" width="100%" height="100%"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ style="overflow: visible; text-align: left;">
+ <div xmlns="http://www.w3.org/1999/xhtml"
+ style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 48px; margin-left: 9px;">
+ <div data-drawio-colors="color: rgb(0, 0, 0); "
+ style="box-sizing: border-box; font-size: 0px; text-align: center;">
+ <div
+ style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
+ diagram</div>
+ </div>
+ </div>
+ </foreignObject>
+ <text x="88" y="52" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px"
+ text-anchor="middle">diagram</text>
+ </switch>
+ </g>
+ </g>
+ <switch>
+ <g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" />
+ <a transform="translate(0,-5)"
+ xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
+ <text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text>
+ </a>
+ </switch>
+</svg>
diff --git a/spec/fixtures/lib/gitlab/email/basic.html b/spec/fixtures/lib/gitlab/email/basic.html
index 807b23c46e3..8c2c4c116b8 100644
--- a/spec/fixtures/lib/gitlab/email/basic.html
+++ b/spec/fixtures/lib/gitlab/email/basic.html
@@ -7,17 +7,17 @@
Even though it has whitespace and newlines, the e-mail converter
will handle it correctly.
- <p><em>Even</em> mismatched tags.</p>
+ <p><em class="class" style="color:red" title="strong">Even</em> mismatched tags.</p>
<div>A div</div>
<div>Another div</div>
- <div>A div<div><strong>within</strong> a div</div></div>
+ <div>A div<div><strong class="class" style="color:red" title="strong">within</strong> a div</div></div>
<p>Another line<br />Yet another line</p>
<a href="http://foo.com">A link</a>
- <p><details><summary>One</summary>Some details</details></p>
+ <p><details class="class" style="color:red" title="strong"><summary>One</summary>Some details</details></p>
<p><details><summary>Two</summary>Some details</details></p>
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index 0bca7b0f494..a0ac70d7d9c 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -7327,7 +7327,7 @@
"status": 1,
"created_at": "2016-03-22T15:44:44.772Z",
"updated_at": "2016-03-29T06:44:44.634Z",
- "statuses": [
+ "builds": [
{
"id": 71,
"project_id": 5,
@@ -7364,7 +7364,41 @@
"artifacts_file_store": 1,
"artifacts_metadata_store": 1,
"artifacts_size": 10
- },
+ }
+ ],
+ "bridges": [
+ {
+ "id": 72,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.",
+ "created_at": "2016-03-22T15:20:35.777Z",
+ "updated_at": "2016-03-22T15:20:35.777Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ deploy command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "deploy",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "stage_id": 12,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ],
+ "generic_commit_statuses": [
{
"id": 72,
"project_id": 5,
@@ -7435,7 +7469,7 @@
"status": 1,
"created_at": "2016-03-22T15:44:44.772Z",
"updated_at": "2016-03-29T06:44:44.634Z",
- "statuses": [
+ "builds": [
{
"id": 74,
"project_id": 5,
@@ -7549,7 +7583,7 @@
"status": 1,
"created_at": "2016-03-22T15:44:44.772Z",
"updated_at": "2016-03-29T06:44:44.634Z",
- "statuses": [
+ "builds": [
{
"id": 76,
"project_id": 5,
@@ -7637,7 +7671,7 @@
"status": 1,
"created_at": "2016-03-22T15:44:44.772Z",
"updated_at": "2016-03-29T06:44:44.634Z",
- "statuses": [
+ "builds": [
{
"id": 78,
"project_id": 5,
@@ -7843,6 +7877,95 @@
}
}
],
+ "commit_notes": [
+ {
+ "note": "Commit note 1",
+ "noteable_type": "Commit",
+ "author_id": 1,
+ "created_at": "2023-01-30T19:27:36.585Z",
+ "updated_at": "2023-02-10T14:43:01.308Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": "sha-notes",
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": 1,
+ "type": null,
+ "position": null,
+ "original_position": null,
+ "resolved_at": null,
+ "resolved_by_id": null,
+ "discussion_id": "e3fde7d585c6467a7a5147e83617eb6daa61aaf4",
+ "change_position": null,
+ "resolved_by_push": null,
+ "confidential": null,
+ "last_edited_at": "2023-02-10T14:43:01.306Z",
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+ {
+ "project_id": 1,
+ "author_id": 1,
+ "created_at": "2023-01-30T19:27:36.815Z",
+ "updated_at": "2023-01-30T19:27:36.815Z",
+ "action": "commented",
+ "target_type": "Note",
+ "fingerprint": null,
+ "push_event_payload": {
+ "commit_count": 1,
+ "action": "pushed",
+ "ref_type": "branch",
+ "commit_to": "sha-notes",
+ "ref": "master"
+ }
+ }
+ ]
+ },
+ {
+ "note": "Commit note 2",
+ "noteable_type": "Commit",
+ "author_id": 1,
+ "created_at": "2023-02-10T14:44:08.138Z",
+ "updated_at": "2023-02-10T14:54:42.828Z",
+ "project_id": 1,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": "sha-notes",
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": 1,
+ "type": null,
+ "position": null,
+ "original_position": null,
+ "resolved_at": null,
+ "resolved_by_id": null,
+ "discussion_id": "53ca55a01732aff4f17daecdf076853f4ab152eb",
+ "change_position": null,
+ "resolved_by_push": null,
+ "confidential": null,
+ "last_edited_at": "2023-02-10T14:54:42.827Z",
+ "author": {
+ "name": "Administrator"
+ },
+ "events": [
+ {
+ "project_id": 1,
+ "author_id": 1,
+ "created_at": "2023-02-10T16:37:16.659Z",
+ "updated_at": "2023-02-10T16:37:16.659Z",
+ "action": "commented",
+ "target_type": "Note",
+ "fingerprint": null
+ }
+ ]
+ }
+ ],
"pipeline_schedules": [
{
"id": 1,
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
index cadaa5abfcd..348a01372ab 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
@@ -1,7 +1,7 @@
{"id":19,"project_id":5,"ref":"master","sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":24,"project_id":5,"pipeline_id":40,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":79,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.695Z","trace":"Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.","created_at":"2016-03-22T15:20:35.950Z","updated_at":"2016-03-29T06:28:12.696Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":40,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":80,"project_id":5,"status":"success","finished_at":null,"trace":"Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.","created_at":"2016-03-22T15:20:35.966Z","updated_at":"2016-03-22T15:20:35.966Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":40,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
{"id":20,"project_id":5,"ref":"master","sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[],"source":"external_pull_request_event","external_pull_request":{"id":3,"pull_request_iid":4,"source_branch":"feature","target_branch":"master","source_repository":"the-repository","target_repository":"the-repository","source_sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","target_sha":"a09386439ca39abe575675ffd4b89ae824fec22f","status":"open","created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z"}}
-{"id":26,"project_id":5,"ref":"master","sha":"048721d90c449b244b7b4c53a9186b04330174ec","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.757Z","updated_at":"2016-03-22T15:20:35.757Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"source":"merge_request_event","merge_request_id":27,"stages":[{"id":21,"project_id":5,"pipeline_id":37,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":74,"project_id":5,"status":"success","finished_at":null,"trace":"Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.","created_at":"2016-03-22T15:20:35.846Z","updated_at":"2016-03-22T15:20:35.846Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":73,"project_id":5,"status":"canceled","finished_at":null,"trace":null,"created_at":"2016-03-22T15:20:35.842Z","updated_at":"2016-03-22T15:20:35.842Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}],"merge_request":{"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}}}
-{"id":36,"project_id":5,"ref":null,"sha":"sha-notes","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.755Z","updated_at":"2016-03-22T15:20:35.755Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"user_id":2147483547,"duration":null,"source":"push","merge_request_id":null,"pipeline_metadata": {"id": 2, "project_id": 5, "pipeline_id": 36, "name": "Build pipeline"},"notes":[{"id":2147483547,"note":"Natus rerum qui dolorem dolorum voluptas.","noteable_type":"Commit","author_id":1,"created_at":"2016-03-22T15:19:59.469Z","updated_at":"2016-03-22T15:19:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":"be93687618e4b132087f430a4d8fc3a609c9b77c","noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"}}],"stages":[{"id":11,"project_id":5,"pipeline_id":36,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":71,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.630Z","trace":null,"created_at":"2016-03-22T15:20:35.772Z","updated_at":"2016-03-29T06:28:12.634Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":{"image":"busybox:latest"},"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"stage_id":11,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null,"type":"Ci::Build","token":"abcd","artifacts_file_store":1,"artifacts_metadata_store":1,"artifacts_size":10},{"id":72,"project_id":5,"status":"success","finished_at":null,"trace":"Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.","created_at":"2016-03-22T15:20:35.777Z","updated_at":"2016-03-22T15:20:35.777Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ deploy command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"deploy","trigger_request_id":null,"stage_idx":1,"stage_id":12,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]},{"id":12,"project_id":5,"pipeline_id":36,"name":"deploy","status":2,"created_at":"2016-03-22T15:45:45.772Z","updated_at":"2016-03-29T06:45:45.634Z"}]}
-{"id":38,"iid":1,"project_id":5,"ref":"master","sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.759Z","updated_at":"2016-03-22T15:20:35.759Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":22,"project_id":5,"pipeline_id":38,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":76,"project_id":5,"status":"success","finished_at":null,"trace":"Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.","created_at":"2016-03-22T15:20:35.882Z","updated_at":"2016-03-22T15:20:35.882Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":75,"project_id":5,"status":"failed","finished_at":null,"trace":"Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.","created_at":"2016-03-22T15:20:35.864Z","updated_at":"2016-03-22T15:20:35.864Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
-{"id":39,"project_id":5,"ref":"master","sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.761Z","updated_at":"2016-03-22T15:20:35.761Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":23,"project_id":5,"pipeline_id":39,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":78,"project_id":5,"status":"success","finished_at":null,"trace":"Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.","created_at":"2016-03-22T15:20:35.927Z","updated_at":"2016-03-22T15:20:35.927Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":77,"project_id":5,"status":"failed","finished_at":null,"trace":"Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.","created_at":"2016-03-22T15:20:35.905Z","updated_at":"2016-03-22T15:20:35.905Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
+{"id":26,"project_id":5,"ref":"master","sha":"048721d90c449b244b7b4c53a9186b04330174ec","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.757Z","updated_at":"2016-03-22T15:20:35.757Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"source":"merge_request_event","merge_request_id":27,"stages":[{"id":21,"project_id":5,"pipeline_id":37,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","builds":[{"id":74,"project_id":5,"status":"success","finished_at":null,"trace":"Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.","created_at":"2016-03-22T15:20:35.846Z","updated_at":"2016-03-22T15:20:35.846Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":73,"project_id":5,"status":"canceled","finished_at":null,"trace":null,"created_at":"2016-03-22T15:20:35.842Z","updated_at":"2016-03-22T15:20:35.842Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}],"merge_request":{"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}}}
+{"id":36,"project_id":5,"ref":null,"sha":"sha-notes","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.755Z","updated_at":"2016-03-22T15:20:35.755Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"user_id":2147483547,"duration":null,"source":"push","merge_request_id":null,"pipeline_metadata": {"id": 2, "project_id": 5, "pipeline_id": 36, "name": "Build pipeline"},"notes":[{"id":2147483547,"note":"Natus rerum qui dolorem dolorum voluptas.","noteable_type":"Commit","author_id":1,"created_at":"2016-03-22T15:19:59.469Z","updated_at":"2016-03-22T15:19:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":"be93687618e4b132087f430a4d8fc3a609c9b77c","noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"}}],"stages":[{"id":11,"project_id":5,"pipeline_id":36,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","builds":[{"id":71,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.630Z","trace":null,"created_at":"2016-03-22T15:20:35.772Z","updated_at":"2016-03-29T06:28:12.634Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":{"image":"busybox:latest"},"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"stage_id":11,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null,"type":"Ci::Build","token":"abcd","artifacts_file_store":1,"artifacts_metadata_store":1,"artifacts_size":10}],"bridges":[{"id":72,"project_id":5,"status":"success","finished_at":null,"trace":"Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.","created_at":"2016-03-22T15:20:35.777Z","updated_at":"2016-03-22T15:20:35.777Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ deploy command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"deploy","trigger_request_id":null,"stage_idx":1,"stage_id":12,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}], "generic_commit_statuses": [{"id":72,"project_id":5,"status":"success","finished_at":null,"trace":"Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.","created_at":"2016-03-22T15:20:35.777Z","updated_at":"2016-03-22T15:20:35.777Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ deploy command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"deploy","trigger_request_id":null,"stage_idx":1,"stage_id":12,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]},{"id":12,"project_id":5,"pipeline_id":36,"name":"deploy","status":2,"created_at":"2016-03-22T15:45:45.772Z","updated_at":"2016-03-29T06:45:45.634Z"}]}
+{"id":38,"iid":1,"project_id":5,"ref":"master","sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.759Z","updated_at":"2016-03-22T15:20:35.759Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":22,"project_id":5,"pipeline_id":38,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","builds":[{"id":76,"project_id":5,"status":"success","finished_at":null,"trace":"Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.","created_at":"2016-03-22T15:20:35.882Z","updated_at":"2016-03-22T15:20:35.882Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":75,"project_id":5,"status":"failed","finished_at":null,"trace":"Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.","created_at":"2016-03-22T15:20:35.864Z","updated_at":"2016-03-22T15:20:35.864Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
+{"id":39,"project_id":5,"ref":"master","sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.761Z","updated_at":"2016-03-22T15:20:35.761Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":23,"project_id":5,"pipeline_id":39,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","builds":[{"id":78,"project_id":5,"status":"success","finished_at":null,"trace":"Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.","created_at":"2016-03-22T15:20:35.927Z","updated_at":"2016-03-22T15:20:35.927Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":77,"project_id":5,"status":"failed","finished_at":null,"trace":"Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.","created_at":"2016-03-22T15:20:35.905Z","updated_at":"2016-03-22T15:20:35.905Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
{"id":41,"project_id":5,"ref":"master","sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/commit_notes.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/commit_notes.ndjson
new file mode 100644
index 00000000000..b623c388b4f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/commit_notes.ndjson
@@ -0,0 +1,2 @@
+{"note":"Commit note 1","noteable_type":"Commit","author_id":1,"created_at":"2023-01-30T19:27:36.585Z","updated_at":"2023-02-10T14:43:01.308Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":"sha-notes","system":false,"st_diff":null,"updated_by_id":1,"type":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"e3fde7d585c6467a7a5147e83617eb6daa61aaf4","change_position":null,"resolved_by_push":null,"confidential":null,"last_edited_at":"2023-02-10T14:43:01.306Z","author":{"name":"Administrator"},"events":[{"project_id":1,"author_id":1,"created_at":"2023-01-30T19:27:36.815Z","updated_at":"2023-01-30T19:27:36.815Z","action":"commented","target_type":"Note","fingerprint":null,"push_event_payload":{"commit_count":1,"action":"pushed","ref_type":"branch","commit_to":"sha-notes","ref":"master"}}]}
+{"note":"Commit note 2","noteable_type":"Commit","author_id":1,"created_at":"2023-02-10T14:44:08.138Z","updated_at":"2023-02-10T14:54:42.828Z","project_id":1,"attachment":{"url":null},"line_code":null,"commit_id":"sha-notes","system":false,"st_diff":null,"updated_by_id":1,"type":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"53ca55a01732aff4f17daecdf076853f4ab152eb","change_position":null,"resolved_by_push":null,"confidential":null,"last_edited_at":"2023-02-10T14:54:42.827Z","author":{"name":"Administrator"},"events":[{"project_id":1,"author_id":1,"created_at":"2023-02-10T16:37:16.659Z","updated_at":"2023-02-10T16:37:16.659Z","action":"commented","target_type":"Note","fingerprint":null}]}
diff --git a/spec/fixtures/packages/debian/README.md b/spec/fixtures/packages/debian/README.md
index e398222ce62..af8e19a2de8 100644
--- a/spec/fixtures/packages/debian/README.md
+++ b/spec/fixtures/packages/debian/README.md
@@ -10,7 +10,7 @@ Go to the `spec/fixtures/packages/debian` directory and clean up old files:
```shell
cd spec/fixtures/packages/debian
-rm -v *.tar.* *.dsc *.deb *.udeb *.buildinfo *.changes
+rm -v *.tar.* *.dsc *.deb *.udeb *.ddeb *.buildinfo *.changes
```
Go to the package source directory and build:
diff --git a/spec/fixtures/packages/debian/sample-ddeb_1.2.3~alpha2_amd64.ddeb b/spec/fixtures/packages/debian/sample-ddeb_1.2.3~alpha2_amd64.ddeb
new file mode 100644
index 00000000000..fb4631219df
--- /dev/null
+++ b/spec/fixtures/packages/debian/sample-ddeb_1.2.3~alpha2_amd64.ddeb
Binary files differ
diff --git a/spec/fixtures/packages/debian/sample/debian/.gitignore b/spec/fixtures/packages/debian/sample/debian/.gitignore
index cb63a746c89..5eb910775ff 100644
--- a/spec/fixtures/packages/debian/sample/debian/.gitignore
+++ b/spec/fixtures/packages/debian/sample/debian/.gitignore
@@ -5,4 +5,4 @@ files
libsample0
sample-dev
sample-udeb
-
+sample-ddeb
diff --git a/spec/fixtures/packages/debian/sample/debian/control b/spec/fixtures/packages/debian/sample/debian/control
index 26d84e1c35d..f9f1b29e3a6 100644
--- a/spec/fixtures/packages/debian/sample/debian/control
+++ b/spec/fixtures/packages/debian/sample/debian/control
@@ -33,3 +33,7 @@ Package-Type: udeb
Architecture: any
Depends: installed-base
Description: Some mostly empty udeb
+
+Package: sample-ddeb
+Architecture: any
+Description: Some fake Ubuntu ddeb
diff --git a/spec/fixtures/packages/debian/sample/debian/rules b/spec/fixtures/packages/debian/sample/debian/rules
index 8ae87843489..9d55aace045 100755
--- a/spec/fixtures/packages/debian/sample/debian/rules
+++ b/spec/fixtures/packages/debian/sample/debian/rules
@@ -1,6 +1,13 @@
#!/usr/bin/make -f
%:
dh $@
+
override_dh_gencontrol:
dh_gencontrol -psample-dev -- -v'1.2.3~binary'
dh_gencontrol --remaining-packages
+
+override_dh_builddeb:
+ # Hack to mimic Ubuntu ddebs
+ dh_builddeb
+ mv ../sample-ddeb_1.2.3~alpha2_amd64.deb ../sample-ddeb_1.2.3~alpha2_amd64.ddeb
+ sed -i 's/sample-ddeb_1.2.3~alpha2_amd64.deb libs optional/sample-ddeb_1.2.3~alpha2_amd64.ddeb libs optional/' debian/files \ No newline at end of file
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc
index 4a5755cd612..21539c229e3 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc
@@ -1,6 +1,6 @@
Format: 3.0 (native)
Source: sample
-Binary: sample-dev, libsample0, sample-udeb
+Binary: sample-dev, libsample0, sample-udeb, sample-ddeb
Architecture: any
Version: 1.2.3~alpha2
Maintainer: John Doe <john.doe@example.com>
@@ -9,11 +9,12 @@ Standards-Version: 4.5.0
Build-Depends: debhelper-compat (= 13)
Package-List:
libsample0 deb libs optional arch=any
+ sample-ddeb deb libs optional arch=any
sample-dev deb libdevel optional arch=any
sample-udeb udeb libs optional arch=any
Checksums-Sha1:
- c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
+ 4a9cb2a7c77a68dc0fe54ba8ecef133a7c949e9d 964 sample_1.2.3~alpha2.tar.xz
Checksums-Sha256:
- 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
+ c9d05185ca158bb804977fa9d7b922e8a0f644a2da41f99d2787dd61b1e2e2c5 964 sample_1.2.3~alpha2.tar.xz
Files:
- d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz
+ adc69e57cda38d9bb7c8d59cacfb6869 964 sample_1.2.3~alpha2.tar.xz
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz
index 2bad3f065b8..d4a43b01821 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz
Binary files differ
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo
index 36e2390b8c7..7bbf1517a53 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo
@@ -1,186 +1,198 @@
Format: 1.0
Source: sample
-Binary: libsample0 sample-dev sample-udeb
+Binary: libsample0 sample-ddeb sample-dev sample-udeb
Architecture: amd64 source
Version: 1.2.3~alpha2
Checksums-Md5:
- ceccb6bb3e45ce6550b24234d4023e0f 671 sample_1.2.3~alpha2.dsc
+ 629921cfc477bfa84adfd2ccaba89783 724 sample_1.2.3~alpha2.dsc
fb0842b21adc44207996296fe14439dd 1124 libsample0_1.2.3~alpha2_amd64.deb
+ 90d1107471eed48c73ad78b19ac83639 1068 sample-ddeb_1.2.3~alpha2_amd64.ddeb
5fafc04dcae1525e1367b15413e5a5c7 1164 sample-dev_1.2.3~binary_amd64.deb
72b1dd7d98229e2fb0355feda1d3a165 736 sample-udeb_1.2.3~alpha2_amd64.udeb
Checksums-Sha1:
- 375ba20ea1789e1e90d469c3454ce49a431d0442 671 sample_1.2.3~alpha2.dsc
+ 443c98a4cf4acd21e2259ae8f2d60fc9932de353 724 sample_1.2.3~alpha2.dsc
5248b95600e85bfe7f63c0dfce330a75f5777366 1124 libsample0_1.2.3~alpha2_amd64.deb
+ 9c5af97cf8dfbe8126c807f540c88757f382b307 1068 sample-ddeb_1.2.3~alpha2_amd64.ddeb
fcd5220b1501ec150ccf37f06e4da919a8612be4 1164 sample-dev_1.2.3~binary_amd64.deb
e42e8f2fe04ed1bb73b44a187674480d0e49dcba 736 sample-udeb_1.2.3~alpha2_amd64.udeb
Checksums-Sha256:
- 81fc156ba937cdb6215362cc4bf6b8dc47be9b4253ba0f1a4ab10c7ea0c4c4e5 671 sample_1.2.3~alpha2.dsc
+ f91070524a59bbb3a1f05a78409e92cb9ee86470b34018bc0b93bd5b2dd3868c 724 sample_1.2.3~alpha2.dsc
1c383a525bfcba619c7305ccd106d61db501a6bbaf0003bf8d0c429fbdb7fcc1 1124 libsample0_1.2.3~alpha2_amd64.deb
+ a6bcc8a4b010f99ce0ea566ac69088e1910e754593c77f2b4942e3473e784e4d 1068 sample-ddeb_1.2.3~alpha2_amd64.ddeb
b8aa8b73a14bc1e0012d4c5309770f5160a8ea7f9dfe6f45222ea6b8a3c35325 1164 sample-dev_1.2.3~binary_amd64.deb
2b0c152b3ab4cc07663350424de972c2b7621d69fe6df2e0b94308a191e4632f 736 sample-udeb_1.2.3~alpha2_amd64.udeb
Build-Origin: Debian
Build-Architecture: amd64
-Build-Date: Fri, 14 May 2021 16:51:32 +0200
+Build-Date: Sat, 04 Mar 2023 09:42:57 +0100
Build-Tainted-By:
merged-usr-via-aliased-dirs
usr-local-has-includes
usr-local-has-libraries
usr-local-has-programs
Installed-Build-Depends:
- autoconf (= 2.69-14),
- automake (= 1:1.16.3-2),
- autopoint (= 0.21-4),
- autotools-dev (= 20180224.1+nmu1),
- base-files (= 11),
- base-passwd (= 3.5.49),
- bash (= 5.1-2+b1),
- binutils (= 2.35.2-2),
- binutils-common (= 2.35.2-2),
- binutils-x86-64-linux-gnu (= 2.35.2-2),
- bsdextrautils (= 2.36.1-7),
+ autoconf (= 2.71-3),
+ automake (= 1:1.16.5-1.3),
+ autopoint (= 0.21-11),
+ autotools-dev (= 20220109.1),
+ base-files (= 12.3),
+ base-passwd (= 3.6.1),
+ bash (= 5.2.15-2+b1),
+ binutils (= 2.40-2),
+ binutils-common (= 2.40-2),
+ binutils-x86-64-linux-gnu (= 2.40-2),
+ bsdextrautils (= 2.38.1-4),
bsdmainutils (= 12.1.7+nmu3),
- bsdutils (= 1:2.36.1-7),
+ bsdutils (= 1:2.38.1-4),
build-essential (= 12.9),
- bzip2 (= 1.0.8-4),
- coreutils (= 8.32-4+b1),
- cpp (= 4:10.2.1-1),
- cpp-10 (= 10.2.1-6),
- cpp-9 (= 9.3.0-22),
- dash (= 0.5.11+git20200708+dd9ef66-5),
- debconf (= 1.5.75),
- debhelper (= 13.3.4),
- debianutils (= 4.11.2),
+ bzip2 (= 1.0.8-5+b1),
+ coreutils (= 9.1-1),
+ cpp (= 4:12.2.0-3),
+ cpp-10 (= 10.4.0-7),
+ cpp-12 (= 12.2.0-14),
+ dash (= 0.5.12-2),
+ debconf (= 1.5.82),
+ debhelper (= 13.11.4),
+ debianutils (= 5.7-0.4),
dh-autoreconf (= 20),
- dh-strip-nondeterminism (= 1.11.0-1),
- diffutils (= 1:3.7-5),
- dpkg (= 1.20.9),
- dpkg-dev (= 1.20.9),
- dwz (= 0.13+20210201-1),
- file (= 1:5.39-3),
- findutils (= 4.8.0-1),
- g++ (= 4:10.2.1-1),
- g++-10 (= 10.2.1-6),
- gcc (= 4:10.2.1-1),
- gcc-10 (= 10.2.1-6),
- gcc-10-base (= 10.2.1-6),
- gcc-9 (= 9.3.0-22),
- gcc-9-base (= 9.3.0-22),
- gettext (= 0.21-4),
- gettext-base (= 0.21-4),
- grep (= 3.6-1),
- groff-base (= 1.22.4-6),
- gzip (= 1.10-4),
- hostname (= 3.23),
- init-system-helpers (= 1.60),
- intltool-debian (= 0.35.0+20060710.5),
- libacl1 (= 2.2.53-10),
+ dh-strip-nondeterminism (= 1.13.1-1),
+ diffutils (= 1:3.8-4),
+ dpkg (= 1.21.20),
+ dpkg-dev (= 1.21.20),
+ dwz (= 0.15-1),
+ file (= 1:5.44-3),
+ findutils (= 4.9.0-4),
+ g++ (= 4:12.2.0-3),
+ g++-12 (= 12.2.0-14),
+ gcc (= 4:12.2.0-3),
+ gcc-10 (= 10.4.0-7),
+ gcc-10-base (= 10.4.0-7),
+ gcc-11-base (= 11.3.0-11),
+ gcc-12 (= 12.2.0-14),
+ gcc-12-base (= 12.2.0-14),
+ gettext (= 0.21-11),
+ gettext-base (= 0.21-11),
+ grep (= 3.8-5),
+ groff-base (= 1.22.4-9),
+ gzip (= 1.12-1),
+ hostname (= 3.23+nmu1),
+ init-system-helpers (= 1.65.2),
+ intltool-debian (= 0.35.0+20060710.6),
+ libacl1 (= 2.3.1-3),
libarchive-zip-perl (= 1.68-1),
- libasan5 (= 9.3.0-22),
- libasan6 (= 10.2.1-6),
- libatomic1 (= 10.2.1-6),
- libattr1 (= 1:2.4.48-6),
- libaudit-common (= 1:3.0-2),
- libaudit1 (= 1:3.0-2),
- libbinutils (= 2.35.2-2),
- libblkid1 (= 2.36.1-7),
- libbz2-1.0 (= 1.0.8-4),
- libc-bin (= 2.31-11),
- libc-dev-bin (= 2.31-11),
- libc6 (= 2.31-11),
- libc6-dev (= 2.31-11),
- libcap-ng0 (= 0.7.9-2.2+b1),
- libcc1-0 (= 10.2.1-6),
- libcom-err2 (= 1.46.2-1),
- libcrypt-dev (= 1:4.4.18-2),
- libcrypt1 (= 1:4.4.18-2),
- libctf-nobfd0 (= 2.35.2-2),
- libctf0 (= 2.35.2-2),
- libdb5.3 (= 5.3.28+dfsg1-0.8),
- libdebconfclient0 (= 0.257),
- libdebhelper-perl (= 13.3.4),
- libdpkg-perl (= 1.20.9),
- libelf1 (= 0.183-1),
- libfile-stripnondeterminism-perl (= 1.11.0-1),
- libgcc-10-dev (= 10.2.1-6),
- libgcc-9-dev (= 9.3.0-22),
- libgcc-s1 (= 10.2.1-6),
- libgcrypt20 (= 1.8.7-3),
- libgdbm-compat4 (= 1.19-2),
- libgdbm6 (= 1.19-2),
- libgmp10 (= 2:6.2.1+dfsg-1),
- libgomp1 (= 10.2.1-6),
- libgpg-error0 (= 1.38-2),
- libgssapi-krb5-2 (= 1.18.3-5),
- libicu67 (= 67.1-6),
- libisl23 (= 0.23-1),
- libitm1 (= 10.2.1-6),
- libk5crypto3 (= 1.18.3-5),
- libkeyutils1 (= 1.6.1-2),
- libkrb5-3 (= 1.18.3-5),
- libkrb5support0 (= 1.18.3-5),
- liblsan0 (= 10.2.1-6),
- liblz4-1 (= 1.9.3-1),
- liblzma5 (= 5.2.5-2),
- libmagic-mgc (= 1:5.39-3),
- libmagic1 (= 1:5.39-3),
- libmount1 (= 2.36.1-7),
- libmpc3 (= 1.2.0-1),
- libmpfr6 (= 4.1.0-3),
+ libasan6 (= 11.3.0-11),
+ libasan8 (= 12.2.0-14),
+ libatomic1 (= 12.2.0-14),
+ libattr1 (= 1:2.5.1-4),
+ libaudit-common (= 1:3.0.9-1),
+ libaudit1 (= 1:3.0.9-1),
+ libbinutils (= 2.40-2),
+ libblkid1 (= 2.38.1-4),
+ libbz2-1.0 (= 1.0.8-5+b1),
+ libc-bin (= 2.36-8),
+ libc-dev-bin (= 2.36-8),
+ libc6 (= 2.36-8),
+ libc6-dev (= 2.36-8),
+ libcap-ng0 (= 0.8.3-1+b3),
+ libcap2 (= 1:2.66-3),
+ libcc1-0 (= 12.2.0-14),
+ libcom-err2 (= 1.46.6-1),
+ libcrypt-dev (= 1:4.4.33-2),
+ libcrypt1 (= 1:4.4.33-2),
+ libctf-nobfd0 (= 2.40-2),
+ libctf0 (= 2.40-2),
+ libdb5.3 (= 5.3.28+dfsg2-1),
+ libdebconfclient0 (= 0.267),
+ libdebhelper-perl (= 13.11.4),
+ libdpkg-perl (= 1.21.20),
+ libelf1 (= 0.188-2.1),
+ libfile-find-rule-perl (= 0.34-3),
+ libfile-stripnondeterminism-perl (= 1.13.1-1),
+ libgcc-10-dev (= 10.4.0-7),
+ libgcc-12-dev (= 12.2.0-14),
+ libgcc-s1 (= 12.2.0-14),
+ libgcrypt20 (= 1.10.1-3),
+ libgdbm-compat4 (= 1.23-3),
+ libgdbm6 (= 1.23-3),
+ libgmp10 (= 2:6.2.1+dfsg1-1.1),
+ libgomp1 (= 12.2.0-14),
+ libgpg-error0 (= 1.46-1),
+ libgprofng0 (= 2.40-2),
+ libgssapi-krb5-2 (= 1.20.1-1),
+ libicu72 (= 72.1-3),
+ libisl23 (= 0.25-1),
+ libitm1 (= 12.2.0-14),
+ libjansson4 (= 2.14-2),
+ libk5crypto3 (= 1.20.1-1),
+ libkeyutils1 (= 1.6.3-2),
+ libkrb5-3 (= 1.20.1-1),
+ libkrb5support0 (= 1.20.1-1),
+ liblsan0 (= 12.2.0-14),
+ liblz4-1 (= 1.9.4-1),
+ liblzma5 (= 5.4.1-0.1),
+ libmagic-mgc (= 1:5.44-3),
+ libmagic1 (= 1:5.44-3),
+ libmd0 (= 1.0.4-2),
+ libmount1 (= 2.38.1-4),
+ libmpc3 (= 1.3.1-1),
+ libmpfr6 (= 4.2.0-1),
libnsl-dev (= 1.3.0-2),
libnsl2 (= 1.3.0-2),
- libpam-modules (= 1.4.0-7),
- libpam-modules-bin (= 1.4.0-7),
- libpam-runtime (= 1.4.0-7),
- libpam0g (= 1.4.0-7),
- libpcre2-8-0 (= 10.36-2),
- libpcre3 (= 2:8.39-13),
- libperl5.32 (= 5.32.1-4),
- libpipeline1 (= 1.5.3-1),
- libquadmath0 (= 10.2.1-6),
- libseccomp2 (= 2.5.1-1),
- libselinux1 (= 3.1-3),
- libsigsegv2 (= 2.13-1),
- libsmartcols1 (= 2.36.1-7),
- libssl1.1 (= 1.1.1k-1),
- libstdc++-10-dev (= 10.2.1-6),
- libstdc++6 (= 10.2.1-6),
- libsub-override-perl (= 0.09-2),
- libsystemd0 (= 247.3-5),
- libtinfo6 (= 6.2+20201114-2),
- libtirpc-common (= 1.3.1-1),
- libtirpc-dev (= 1.3.1-1),
- libtirpc3 (= 1.3.1-1),
- libtool (= 2.4.6-15),
- libtsan0 (= 10.2.1-6),
- libubsan1 (= 10.2.1-6),
+ libnumber-compare-perl (= 0.03-3),
+ libpam-modules (= 1.5.2-6),
+ libpam-modules-bin (= 1.5.2-6),
+ libpam-runtime (= 1.5.2-6),
+ libpam0g (= 1.5.2-6),
+ libpcre2-8-0 (= 10.42-1),
+ libperl5.36 (= 5.36.0-7),
+ libpipeline1 (= 1.5.7-1),
+ libquadmath0 (= 12.2.0-14),
+ libseccomp2 (= 2.5.4-1+b3),
+ libselinux1 (= 3.4-1+b5),
+ libsmartcols1 (= 2.38.1-4),
+ libssl3 (= 3.0.8-1),
+ libstdc++-12-dev (= 12.2.0-14),
+ libstdc++6 (= 12.2.0-14),
+ libsub-override-perl (= 0.09-4),
+ libsystemd0 (= 252.5-2),
+ libtext-glob-perl (= 0.11-3),
+ libtinfo6 (= 6.4-2),
+ libtirpc-common (= 1.3.3+ds-1),
+ libtirpc-dev (= 1.3.3+ds-1),
+ libtirpc3 (= 1.3.3+ds-1),
+ libtool (= 2.4.7-5),
+ libtsan0 (= 11.3.0-11),
+ libtsan2 (= 12.2.0-14),
+ libubsan1 (= 12.2.0-14),
libuchardet0 (= 0.0.7-1),
- libudev1 (= 247.3-5),
- libunistring2 (= 0.9.10-4),
- libuuid1 (= 2.36.1-7),
- libxml2 (= 2.9.10+dfsg-6.3+b1),
- libzstd1 (= 1.4.8+dfsg-2.1),
- linux-libc-dev (= 5.10.28-1),
- login (= 1:4.8.1-1),
- lsb-base (= 11.1.0),
- m4 (= 1.4.18-5),
+ libudev1 (= 252.5-2),
+ libunistring2 (= 1.0-2),
+ libuuid1 (= 2.38.1-4),
+ libxml2 (= 2.9.14+dfsg-1.1+b3),
+ libzstd1 (= 1.5.2+dfsg2-3),
+ linux-libc-dev (= 6.1.8-1),
+ login (= 1:4.13+dfsg1-1),
+ m4 (= 1.4.19-3),
make (= 4.3-4.1),
- man-db (= 2.9.4-2),
- mawk (= 1.3.4.20200120-2),
+ man-db (= 2.11.2-1),
+ mawk (= 1.3.4.20200120-3.1),
ncal (= 12.1.7+nmu3),
- ncurses-base (= 6.2+20201114-2),
- ncurses-bin (= 6.2+20201114-2),
+ ncurses-base (= 6.4-2),
+ ncurses-bin (= 6.4-2),
patch (= 2.7.6-7),
- perl (= 5.32.1-4),
- perl-base (= 5.32.1-4),
- perl-modules-5.32 (= 5.32.1-4),
+ perl (= 5.36.0-7),
+ perl-base (= 5.36.0-7),
+ perl-modules-5.36 (= 5.36.0-7),
po-debconf (= 1.0.21+nmu1),
- sed (= 4.7-1),
- sensible-utils (= 0.0.14),
- sysvinit-utils (= 2.96-7),
+ rpcsvc-proto (= 1.4.3-1),
+ sed (= 4.9-1),
+ sensible-utils (= 0.0.17+nmu1),
+ sysvinit-utils (= 3.06-2),
tar (= 1.34+dfsg-1),
- util-linux (= 2.36.1-7),
- xz-utils (= 5.2.5-2),
- zlib1g (= 1:1.2.11.dfsg-2)
+ usrmerge (= 35),
+ util-linux (= 2.38.1-4),
+ util-linux-extra (= 2.38.1-4),
+ xz-utils (= 5.4.1-0.1),
+ zlib1g (= 1:1.2.13.dfsg-1)
Environment:
DEB_BUILD_OPTIONS="parallel=8"
LANG="fr_FR.UTF-8"
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes
index 7aa4761c49c..a1d2f95e231 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes
@@ -17,23 +17,26 @@ Changes:
.
* Initial release
Checksums-Sha1:
- 375ba20ea1789e1e90d469c3454ce49a431d0442 671 sample_1.2.3~alpha2.dsc
- c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
+ 443c98a4cf4acd21e2259ae8f2d60fc9932de353 724 sample_1.2.3~alpha2.dsc
+ 4a9cb2a7c77a68dc0fe54ba8ecef133a7c949e9d 964 sample_1.2.3~alpha2.tar.xz
5248b95600e85bfe7f63c0dfce330a75f5777366 1124 libsample0_1.2.3~alpha2_amd64.deb
+ 9c5af97cf8dfbe8126c807f540c88757f382b307 1068 sample-ddeb_1.2.3~alpha2_amd64.ddeb
fcd5220b1501ec150ccf37f06e4da919a8612be4 1164 sample-dev_1.2.3~binary_amd64.deb
e42e8f2fe04ed1bb73b44a187674480d0e49dcba 736 sample-udeb_1.2.3~alpha2_amd64.udeb
- 661f7507efa6fdd3763c95581d0baadb978b7ef5 5507 sample_1.2.3~alpha2_amd64.buildinfo
+ bcc4ca85f17a31066b726cd4e04485ab24a682c6 6032 sample_1.2.3~alpha2_amd64.buildinfo
Checksums-Sha256:
- 81fc156ba937cdb6215362cc4bf6b8dc47be9b4253ba0f1a4ab10c7ea0c4c4e5 671 sample_1.2.3~alpha2.dsc
- 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
+ f91070524a59bbb3a1f05a78409e92cb9ee86470b34018bc0b93bd5b2dd3868c 724 sample_1.2.3~alpha2.dsc
+ c9d05185ca158bb804977fa9d7b922e8a0f644a2da41f99d2787dd61b1e2e2c5 964 sample_1.2.3~alpha2.tar.xz
1c383a525bfcba619c7305ccd106d61db501a6bbaf0003bf8d0c429fbdb7fcc1 1124 libsample0_1.2.3~alpha2_amd64.deb
+ a6bcc8a4b010f99ce0ea566ac69088e1910e754593c77f2b4942e3473e784e4d 1068 sample-ddeb_1.2.3~alpha2_amd64.ddeb
b8aa8b73a14bc1e0012d4c5309770f5160a8ea7f9dfe6f45222ea6b8a3c35325 1164 sample-dev_1.2.3~binary_amd64.deb
2b0c152b3ab4cc07663350424de972c2b7621d69fe6df2e0b94308a191e4632f 736 sample-udeb_1.2.3~alpha2_amd64.udeb
- d0c169e9caa5b303a914b27b5adf69768fe6687d4925905b7d0cd9c0f9d4e56c 5507 sample_1.2.3~alpha2_amd64.buildinfo
+ 5a3dac17c4ff0d49fa5f47baa973902b59ad2ee05147062b8ed8f19d196731d1 6032 sample_1.2.3~alpha2_amd64.buildinfo
Files:
- ceccb6bb3e45ce6550b24234d4023e0f 671 libs optional sample_1.2.3~alpha2.dsc
- d5ca476e4229d135a88f9c729c7606c9 864 libs optional sample_1.2.3~alpha2.tar.xz
+ 629921cfc477bfa84adfd2ccaba89783 724 libs optional sample_1.2.3~alpha2.dsc
+ adc69e57cda38d9bb7c8d59cacfb6869 964 libs optional sample_1.2.3~alpha2.tar.xz
fb0842b21adc44207996296fe14439dd 1124 libs optional libsample0_1.2.3~alpha2_amd64.deb
+ 90d1107471eed48c73ad78b19ac83639 1068 libs optional sample-ddeb_1.2.3~alpha2_amd64.ddeb
5fafc04dcae1525e1367b15413e5a5c7 1164 libdevel optional sample-dev_1.2.3~binary_amd64.deb
72b1dd7d98229e2fb0355feda1d3a165 736 libs optional sample-udeb_1.2.3~alpha2_amd64.udeb
- 12a5ac4f16ad75f8741327ac23b4c0d7 5507 libs optional sample_1.2.3~alpha2_amd64.buildinfo
+ cc07ff4d741aec132816f9bd67c6875d 6032 libs optional sample_1.2.3~alpha2_amd64.buildinfo
diff --git a/spec/fixtures/service_account.json b/spec/fixtures/service_account.json
new file mode 100644
index 00000000000..9f7f5526cf5
--- /dev/null
+++ b/spec/fixtures/service_account.json
@@ -0,0 +1,12 @@
+{
+ "type": "service_account",
+ "project_id": "demo-app-123",
+ "private_key_id": "47f0b1700983da548af6fcd37007f42996099999",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nABCDEFIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJn8w20WcN+fi5\nIhO1BEFCv7ExK8J5rW5Pc8XpJgpQoL5cfv6qC6aS+x4maI7S4AG7diqXBLCfjlnA\nqBzXwCRnnPtQhu+v1ehAj5fGNa7F51f9aacRNmKdHzNmWZEPDuLqq0I/Ewcsotu+\nnb+tCYk1o2ahyPZau8JtXFZs7oZb7SrfgoSJemccxeVreGm1Dt6SM74/3qJAeHN/\niK/v0IiQP1GS4Jxgz38XQGo+jiTpNrFcf4S0RNxKcNf+tuuEBDi57LBLwdotM7E5\nF1l9pZZMWkmQKQIxeER6+2HuE56V6QPITwkQ/u9XZFQSgl4SBIw2sHr5D/xaUxjw\n+kMy2Jt9AgMBAAECggEACL7E34rRIWbP043cv3ZQs1RiWzY2mvWmCiMEzkz0rRRv\nyqNv0yXVYtzVV7KjdpY56leLgjM1Sv0PEQoUUtpWFJAXSXdKLaewSXPrpXCoz5OD\nekMgeItnQcE7nECdyAKsCSQw/SXg4t4p0a3WGsCwt3If2TwWIrov9R4zGcn1wMZn\n922WtZDmh2NqdTZIKElWZLxNlIr/1v88mAp7oSa1DLfqWkwEEnxK7GGAiwN8ARIF\nkvgiuKdsHBf5aNKg70xN6AcZx/Z4+KZxXxyKKF5VkjCtDzA97EjJqftDPwGTkela\n2bgiDSJs0Un0wQpFFRDrlfyo7rr9Ey/Gf4rR66NWeQKBgQD7qPP55xoWHCDvoK9P\nMN67qFLNDPWcKVKr8siwUlZ6/+acATXjfNUjsJLM7vBxYLjdtFxQ/vojJTQyMxHt\n80wARDk1DTu2zhltL2rKo6LfbwjQsot1MLZFXAMwqtHTLfURaj8kO1JDV/j+4a94\nP0gzNMiBYAKWm6z08akEz2TrhQKBgQDNGfFvtxo4Mf6AA3iYXCwc0CJXb+cqZkW/\n7glnV+vDqYVo23HJaKHFD+Xqaj+cUrOUNglWgT9WSCZR++Hzw1OCPZvX2V9Z6eQh\ngqOBX6D19q9jfShfxLywEAD5pk7LMINumsNm6H+6shJQK5c67bsM9/KQbSnIlWhw\n7JBe8OlFmQKBgQDREyF2mb/7ZG0ch8N9qB0zjHkV79FRZqdPQUnn6s/8KgO90eei\nUkCFARpE9bF+kBul3UTg6aSIdE0z82fO51VZ11Qrtg3JJtrK8hznsyEKPaX2NI9V\n0h1r7DCeSxw9NS4nxLwmbr4+QqUTpA3yeaiTGiQGD+y2kSkU6nxACclPPQKBgFkb\nkVqg6YJKrjB90ZIYUY3/GzxzwLIaFumpCGretu6eIvkIhiokDExqeNBccuB+ych1\npZ7wrkzVMdjinythzFFEZQXlSdjtlhC9Cj52Bp92GoMV6EmbVwMDIPlVuNvsat3N\n3WFDV+ML5IryNVUD3gVnX/pBgyrDRsnw7VRiRGbZAoGBANxZwGKZo0zpyb5O5hS6\nxVrgJtIySlV5BOEjFXKeLwzByht8HmrHhSWix6WpPejfK1RHhl3boU6t9yeC0cre\nvUI/Y9LBhHXjSwWCWlqVe9yYqsde+xf0UYRS8IoaoJjus7YVJr9yPpCboEF28ZmQ\ndVBlpZYg6oLIar6waaLMz/1B\n-----END PRIVATE KEY-----\n",
+ "client_email": "demo-app-account@demo-app-374914.iam.gserviceaccount.com",
+ "client_id": "111111116847110173051",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/demo-app-account%40demo-app-374914.iam.gserviceaccount.com"
+}
diff --git a/spec/fixtures/structure.sql b/spec/fixtures/structure.sql
index 49061dfa8ea..800c33bb9b9 100644
--- a/spec/fixtures/structure.sql
+++ b/spec/fixtures/structure.sql
@@ -18,3 +18,11 @@ CREATE TABLE ci_project_mirrors (
project_id integer NOT NULL,
namespace_id integer NOT NULL
);
+
+CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1();
+
+CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION my_function();
+
+CREATE TRIGGER missing_trigger_1 BEFORE INSERT OR UPDATE ON public.t3 FOR EACH ROW EXECUTE FUNCTION t3();
+
+CREATE TRIGGER projects_loose_fk_trigger AFTER DELETE ON projects REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
diff --git a/spec/fixtures/work_items_invalid_types.csv b/spec/fixtures/work_items_invalid_types.csv
new file mode 100644
index 00000000000..b82e9035451
--- /dev/null
+++ b/spec/fixtures/work_items_invalid_types.csv
@@ -0,0 +1,4 @@
+title,type
+Invalid issue,isssue
+Invalid Issue,issue!!
+Missing type,
diff --git a/spec/fixtures/work_items_missing_header.csv b/spec/fixtures/work_items_missing_header.csv
new file mode 100644
index 00000000000..1a2e7ed42ab
--- /dev/null
+++ b/spec/fixtures/work_items_missing_header.csv
@@ -0,0 +1,3 @@
+type,created_at
+issue,2021-01-01
+other_issue,2023-02-02
diff --git a/spec/fixtures/work_items_valid.csv b/spec/fixtures/work_items_valid.csv
new file mode 100644
index 00000000000..12f2f8bc816
--- /dev/null
+++ b/spec/fixtures/work_items_valid.csv
@@ -0,0 +1,3 @@
+title,type
+馬のスモモモモモモモモ,Issue
+Issue with alternate case,ISSUE
diff --git a/spec/fixtures/work_items_valid_types.csv b/spec/fixtures/work_items_valid_types.csv
new file mode 100644
index 00000000000..f4cca9e65f1
--- /dev/null
+++ b/spec/fixtures/work_items_valid_types.csv
@@ -0,0 +1,3 @@
+title,type
+Valid issue,issue
+Valid issue with alternate case,ISSUE
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 45639f4c948..200f539fb3e 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -12,6 +12,7 @@ settings:
jest:
jestConfigFile: 'jest.config.js'
rules:
+ '@gitlab/vtu-no-explicit-wrapper-destroy': error
jest/expect-expect:
- off
- assertFunctionNames:
diff --git a/spec/frontend/__helpers__/create_mock_source_editor_extension.js b/spec/frontend/__helpers__/create_mock_source_editor_extension.js
new file mode 100644
index 00000000000..fa529604d6f
--- /dev/null
+++ b/spec/frontend/__helpers__/create_mock_source_editor_extension.js
@@ -0,0 +1,12 @@
+export const createMockSourceEditorExtension = (ActualExtension) => {
+ const { extensionName } = ActualExtension;
+ const providedKeys = Object.keys(new ActualExtension().provides());
+
+ const mockedMethods = Object.fromEntries(providedKeys.map((key) => [key, jest.fn()]));
+ const MockExtension = function MockExtension() {};
+ MockExtension.extensionName = extensionName;
+ MockExtension.mockedMethods = mockedMethods;
+ MockExtension.prototype.provides = jest.fn().mockReturnValue(mockedMethods);
+
+ return MockExtension;
+};
diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js
index d5044be88d7..7e8dd463d28 100644
--- a/spec/frontend/__helpers__/experimentation_helper.js
+++ b/spec/frontend/__helpers__/experimentation_helper.js
@@ -2,16 +2,9 @@ import { merge } from 'lodash';
// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) {
- let origGon;
-
beforeEach(() => {
- origGon = window.gon;
window.gon = merge({}, window.gon || {}, { experiments: { [experimentKey]: value } });
});
-
- afterEach(() => {
- window.gon = origGon;
- });
}
// The following helper is for specs that use `gitlab-experiment` utilities,
diff --git a/spec/frontend/__helpers__/gon_helper.js b/spec/frontend/__helpers__/gon_helper.js
new file mode 100644
index 00000000000..51d5ece5fc1
--- /dev/null
+++ b/spec/frontend/__helpers__/gon_helper.js
@@ -0,0 +1,5 @@
+export const createGon = (IS_EE) => {
+ return {
+ ee: IS_EE,
+ };
+};
diff --git a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
index 54d397d0997..8b6cdedfd9f 100644
--- a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
+++ b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
@@ -12,10 +12,6 @@ describe('keepAlive', () => {
wrapper = mount(keepAlive(component));
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('converts a component to a keep-alive component', async () => {
const { element } = wrapper.findComponent(component);
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 2fe9fe89a90..0217835b2a3 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -1,10 +1,12 @@
/* Common setup for both unit and integration test environments */
+import { ReadableStream, WritableStream } from 'node:stream/web';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import { enableAutoDestroy } from '@vue/test-utils';
import 'jquery';
import Translate from '~/vue_shared/translate';
import setWindowLocation from './set_window_location_helper';
+import { createGon } from './gon_helper';
import { setGlobalDateToFakeDate } from './fake_date';
import { TEST_HOST } from './test_constants';
import * as customMatchers from './matchers';
@@ -13,6 +15,9 @@ import './dom_shims';
import './jquery';
import '~/commons/bootstrap';
+global.ReadableStream = ReadableStream;
+global.WritableStream = WritableStream;
+
enableAutoDestroy(afterEach);
// This module has some fairly decent visual test coverage in it's own repository.
@@ -67,8 +72,13 @@ beforeEach(() => {
// eslint-disable-next-line jest/no-standalone-expect
expect.hasAssertions();
- // Reset the mocked window.location. This ensures tests don't interfere with
- // each other, and removes the need to tidy up if it was changed for a given
- // test.
+ // Reset globals: This ensures tests don't interfere with
+ // each other, and removes the need to tidy up if it was
+ // changed for a given test.
+
+ // Reset the mocked window.location
setWindowLocation(TEST_HOST);
+
+ // Reset window.gon object
+ window.gon = createGon(window.IS_EE);
});
diff --git a/spec/frontend/__helpers__/vue_mock_directive.js b/spec/frontend/__helpers__/vue_mock_directive.js
index e952f258c4d..e7a2aa7f10d 100644
--- a/spec/frontend/__helpers__/vue_mock_directive.js
+++ b/spec/frontend/__helpers__/vue_mock_directive.js
@@ -2,7 +2,7 @@ export const getKey = (name) => `$_gl_jest_${name}`;
export const getBinding = (el, name) => el[getKey(name)];
-const writeBindingToElement = (el, { name, value, arg, modifiers }) => {
+const writeBindingToElement = (el, name, { value, arg, modifiers }) => {
el[getKey(name)] = {
value,
arg,
@@ -10,16 +10,24 @@ const writeBindingToElement = (el, { name, value, arg, modifiers }) => {
};
};
-export const createMockDirective = () => ({
- bind(el, binding) {
- writeBindingToElement(el, binding);
- },
+export const createMockDirective = (name) => {
+ if (!name) {
+ throw new Error(
+ 'Vue 3 no longer passes the name of the directive to its hooks, an explicit name is required',
+ );
+ }
- update(el, binding) {
- writeBindingToElement(el, binding);
- },
+ return {
+ bind(el, binding) {
+ writeBindingToElement(el, name, binding);
+ },
- unbind(el, { name }) {
- delete el[getKey(name)];
- },
-});
+ update(el, binding) {
+ writeBindingToElement(el, name, binding);
+ },
+
+ unbind(el) {
+ delete el[getKey(name)];
+ },
+ };
+};
diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js
index bdd5a0a9034..94164814879 100644
--- a/spec/frontend/__helpers__/vuex_action_helper.js
+++ b/spec/frontend/__helpers__/vuex_action_helper.js
@@ -78,6 +78,8 @@ export default (
}
actions.push(dispatchedAction);
+
+ return Promise.resolve();
};
const validateResults = () => {
diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js
index 4bd21ff150a..64081ca11a3 100644
--- a/spec/frontend/__helpers__/vuex_action_helper_spec.js
+++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js
@@ -83,6 +83,20 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
});
+ describe('given an async action (chaining off a dispatch)', () => {
+ it('mocks dispatch accurately', () => {
+ const asyncAction = ({ commit, dispatch }) => {
+ return dispatch('ACTION').then(() => {
+ commit('MUTATION');
+ });
+ };
+
+ assertion = { actions: [{ type: 'ACTION' }], mutations: [{ type: 'MUTATION' }] };
+
+ return testAction(asyncAction, null, {}, assertion.mutations, assertion.actions);
+ });
+ });
+
describe('given an async action (returning a promise)', () => {
const data = { FOO: 'BAR' };
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 4d893bcd0bd..c51f37db384 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -13,13 +13,18 @@ export * from '@gitlab/ui';
* are imported internally in `@gitlab/ui`.
*/
-jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
+/* eslint-disable global-require */
+
+jest.mock('@gitlab/ui/src/directives/tooltip.js', () => ({
GlTooltipDirective: {
bind() {},
},
}));
+jest.mock('@gitlab/ui/dist/directives/tooltip.js', () =>
+ require('@gitlab/ui/src/directives/tooltip'),
+);
-jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
+jest.mock('@gitlab/ui/src/components/base/tooltip/tooltip.vue', () => ({
props: ['target', 'id', 'triggers', 'placement', 'container', 'boundary', 'disabled', 'show'],
render(h) {
return h(
@@ -33,7 +38,11 @@ jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
},
}));
-jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
+jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () =>
+ require('@gitlab/ui/src/components/base/tooltip/tooltip.vue'),
+);
+
+jest.mock('@gitlab/ui/src/components/base/popover/popover.vue', () => ({
props: {
cssClasses: {
type: Array,
@@ -65,3 +74,6 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
);
},
}));
+jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () =>
+ require('@gitlab/ui/src/components/base/popover/popover.vue'),
+);
diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js
index d4fe2ce5406..15f806fc31a 100644
--- a/spec/frontend/__mocks__/lodash/debounce.js
+++ b/spec/frontend/__mocks__/lodash/debounce.js
@@ -9,9 +9,22 @@
// Further reference: https://github.com/facebook/jest/issues/3465
export default (fn) => {
- const debouncedFn = jest.fn().mockImplementation(fn);
- debouncedFn.cancel = jest.fn();
- debouncedFn.flush = jest.fn().mockImplementation(() => {
+ let id;
+ const debouncedFn = jest.fn(function run(...args) {
+ // this is calculated in runtime so beforeAll hook works in tests
+ const timeout = global.JEST_DEBOUNCE_THROTTLE_TIMEOUT;
+ if (timeout) {
+ id = setTimeout(() => {
+ fn.apply(this, args);
+ }, timeout);
+ } else {
+ fn.apply(this, args);
+ }
+ });
+ debouncedFn.cancel = jest.fn(() => {
+ clearTimeout(id);
+ });
+ debouncedFn.flush = jest.fn(() => {
const errorMessage =
"The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'.";
diff --git a/spec/frontend/__mocks__/lodash/throttle.js b/spec/frontend/__mocks__/lodash/throttle.js
index e8a82654c78..b1014662918 100644
--- a/spec/frontend/__mocks__/lodash/throttle.js
+++ b/spec/frontend/__mocks__/lodash/throttle.js
@@ -1,4 +1,4 @@
// Similar to `lodash/debounce`, `lodash/throttle` also causes flaky specs.
// See `./debounce.js` for more details.
-export default (fn) => fn;
+export { default } from './debounce';
diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
index ec20088c443..5de5f495f01 100644
--- a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
+++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
@@ -33,10 +33,6 @@ describe('AbuseCategorySelector', () => {
createComponent({ showDrawer: true });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findTitle = () => wrapper.findByTestId('category-drawer-title');
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 491d2a0e323..6605faadc17 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -25,10 +25,6 @@ describe('~/access_tokens/components/expires_at_field', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render datepicker with input info', () => {
createComponent();
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 e4313bdfa26..fb92cc34ce9 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
@@ -4,12 +4,12 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from '~/access_tokens/components/constants';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import { __, sprintf } from '~/locale';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('~/access_tokens/components/new_access_token_app', () => {
let wrapper;
@@ -52,7 +52,6 @@ describe('~/access_tokens/components/new_access_token_app', () => {
afterEach(() => {
resetHTMLFixture();
- wrapper.destroy();
createAlert.mockClear();
});
diff --git a/spec/frontend/access_tokens/components/token_spec.js b/spec/frontend/access_tokens/components/token_spec.js
index 1af21aaa8cd..f62f7d72e3b 100644
--- a/spec/frontend/access_tokens/components/token_spec.js
+++ b/spec/frontend/access_tokens/components/token_spec.js
@@ -23,10 +23,6 @@ describe('Token', () => {
wrapper = mountExtended(Token, { propsData: defaultPropsData, slots: defaultSlots });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title slot', () => {
createComponent();
diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js
index d7acfbb47eb..6e7dee6a2cc 100644
--- a/spec/frontend/access_tokens/components/tokens_app_spec.js
+++ b/spec/frontend/access_tokens/components/tokens_app_spec.js
@@ -54,10 +54,6 @@ describe('TokensApp', () => {
expect(container.props()).toMatchObject(expectedProps);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all enabled tokens', () => {
createComponent();
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 1d57473943b..5e96da9af7e 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
@@ -55,10 +55,6 @@ describe('AddContextCommitsModal', () => {
wrapper = createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders modal with 2 tabs', () => {
expect(wrapper.element).toMatchSnapshot();
});
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 f679576182f..975f115c4bb 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
@@ -26,10 +26,6 @@ describe('ReviewTabContainer', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows loading icon when commits are being loaded', () => {
createWrapper({ isLoading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
index 27c8d760a96..3863eee3795 100644
--- a/spec/frontend/add_context_commits_modal/store/actions_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -31,10 +31,10 @@ describe('AddContextCommitsModalStoreActions', () => {
short_id: 'abcdef',
committed_date: '2020-06-12',
};
- gon.api_version = 'v4';
let mock;
beforeEach(() => {
+ gon.api_version = 'v4';
mock = new MockAdapter(axios);
});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
new file mode 100644
index 00000000000..d32fa25d238
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -0,0 +1,43 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseReportRow', () => {
+ let wrapper;
+ const mockAbuseReport = mockAbuseReports[0];
+
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findUpdatedAt = () => wrapper.findByTestId('updated-at');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AbuseReportRow, {
+ propsData: {
+ report: mockAbuseReport,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a ListItem', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ it('displays correctly formatted title', () => {
+ const { reporter, reportedUser, category } = mockAbuseReport;
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `${reportedUser.name} reported for ${category} by ${reporter.name}`,
+ );
+ });
+
+ it('displays correctly formatted updated at', () => {
+ expect(findUpdatedAt().text()).toMatchInterpolatedText(
+ `Updated ${getTimeago().format(mockAbuseReport.updatedAt)}`,
+ );
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
new file mode 100644
index 00000000000..9efab8403a0
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
@@ -0,0 +1,214 @@
+import { shallowMount } from '@vue/test-utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { redirectTo, updateHistory } from '~/lib/utils/url_utility';
+import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue';
+import {
+ FILTERED_SEARCH_TOKENS,
+ FILTERED_SEARCH_TOKEN_USER,
+ FILTERED_SEARCH_TOKEN_REPORTER,
+ FILTERED_SEARCH_TOKEN_STATUS,
+ FILTERED_SEARCH_TOKEN_CATEGORY,
+ DEFAULT_SORT,
+ SORT_OPTIONS,
+} from '~/admin/abuse_reports/constants';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils';
+
+jest.mock('~/lib/utils/url_utility', () => {
+ const urlUtility = jest.requireActual('~/lib/utils/url_utility');
+
+ return {
+ __esModule: true,
+ ...urlUtility,
+ redirectTo: jest.fn(),
+ updateHistory: jest.fn(),
+ };
+});
+
+describe('AbuseReportsFilteredSearchBar', () => {
+ let wrapper;
+
+ const CATEGORIES = ['spam', 'phishing'];
+
+ const createComponent = () => {
+ wrapper = shallowMount(AbuseReportsFilteredSearchBar, {
+ provide: { categories: CATEGORIES },
+ });
+ };
+
+ const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
+
+ beforeEach(() => {
+ setWindowLocation('https://localhost');
+ });
+
+ it('passes correct props to `FilteredSearchBar` component', () => {
+ createComponent();
+
+ const categoryToken = buildFilteredSearchCategoryToken(CATEGORIES);
+
+ expect(findFilteredSearchBar().props()).toMatchObject({
+ namespace: 'abuse_reports',
+ recentSearchesStorageKey: 'abuse_reports',
+ searchInputPlaceholder: 'Filter reports',
+ tokens: [...FILTERED_SEARCH_TOKENS, categoryToken],
+ initialSortBy: DEFAULT_SORT,
+ sortOptions: SORT_OPTIONS,
+ });
+ });
+
+ it('sets status=open query when there is no initial status query', () => {
+ createComponent();
+
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: 'https://localhost/?status=open',
+ replace: true,
+ });
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
+ {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'open', operator: '=' },
+ },
+ ]);
+ });
+
+ it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
+ setWindowLocation('?status=closed&user=mr_abuser&reporter=ms_nitch');
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
+ {
+ type: FILTERED_SEARCH_TOKEN_USER.type,
+ value: { data: 'mr_abuser', operator: '=' },
+ },
+ {
+ type: FILTERED_SEARCH_TOKEN_REPORTER.type,
+ value: { data: 'ms_nitch', operator: '=' },
+ },
+ {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'closed', operator: '=' },
+ },
+ ]);
+ });
+
+ describe('initial sort', () => {
+ it.each(
+ SORT_OPTIONS.flatMap(({ sortDirection: { descending, ascending } }) => [
+ descending,
+ ascending,
+ ]),
+ )(
+ 'parses sort=%s query and passes it to `FilteredSearchBar` component as initialSortBy',
+ (sortBy) => {
+ setWindowLocation(`?sort=${sortBy}`);
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(sortBy);
+ },
+ );
+
+ it(`uses ${DEFAULT_SORT} as initialSortBy when sort query param is invalid`, () => {
+ setWindowLocation(`?sort=unknown`);
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT);
+ });
+ });
+
+ describe('onFilter', () => {
+ const USER_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_USER.type,
+ value: { data: 'mr_abuser', operator: '=' },
+ };
+ const REPORTER_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_REPORTER.type,
+ value: { data: 'ms_nitch', operator: '=' },
+ };
+ const STATUS_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'open', operator: '=' },
+ };
+ const CATEGORY_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_CATEGORY.type,
+ value: { data: 'spam', operator: '=' },
+ };
+
+ const createComponentAndFilter = (filterTokens, initialLocation) => {
+ if (initialLocation) {
+ setWindowLocation(initialLocation);
+ }
+
+ createComponent();
+
+ findFilteredSearchBar().vm.$emit('onFilter', filterTokens);
+ };
+
+ it.each([USER_FILTER_TOKEN, REPORTER_FILTER_TOKEN, STATUS_FILTER_TOKEN, CATEGORY_FILTER_TOKEN])(
+ 'redirects with $type query param',
+ (filterToken) => {
+ createComponentAndFilter([filterToken]);
+ const { type, value } = filterToken;
+ expect(redirectTo).toHaveBeenCalledWith(`https://localhost/?${type}=${value.data}`);
+ },
+ );
+
+ it('ignores search query param', () => {
+ const searchFilterToken = { type: FILTERED_SEARCH_TERM, value: { data: 'ignored' } };
+ createComponentAndFilter([USER_FILTER_TOKEN, searchFilterToken]);
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser');
+ });
+
+ it('redirects without page query param', () => {
+ createComponentAndFilter([USER_FILTER_TOKEN], '?page=2');
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser');
+ });
+
+ it('redirects with existing sort query param', () => {
+ createComponentAndFilter([USER_FILTER_TOKEN], `?sort=${DEFAULT_SORT}`);
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT}`,
+ );
+ });
+ });
+
+ describe('onSort', () => {
+ const SORT_VALUE = 'updated_at_asc';
+ const EXISTING_QUERY = 'status=closed&user=mr_abuser';
+
+ const createComponentAndSort = (initialLocation) => {
+ setWindowLocation(initialLocation);
+ createComponent();
+ findFilteredSearchBar().vm.$emit('onSort', SORT_VALUE);
+ };
+
+ it('redirects to URL with existing query params and the sort query param', () => {
+ createComponentAndSort(`?${EXISTING_QUERY}`);
+
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`,
+ );
+ });
+
+ it('redirects without page query param', () => {
+ createComponentAndSort(`?${EXISTING_QUERY}&page=2`);
+
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`,
+ );
+ });
+
+ it('redirects with existing sort query param replaced with the new one', () => {
+ createComponentAndSort(`?${EXISTING_QUERY}&sort=created_at_desc`);
+
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/app_spec.js b/spec/frontend/admin/abuse_reports/components/app_spec.js
new file mode 100644
index 00000000000..41728baaf33
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/app_spec.js
@@ -0,0 +1,104 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlPagination } from '@gitlab/ui';
+import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import AbuseReportsApp from '~/admin/abuse_reports/components/app.vue';
+import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue';
+import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseReportsApp', () => {
+ let wrapper;
+
+ const findFilteredSearchBar = () => wrapper.findComponent(AbuseReportsFilteredSearchBar);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findAbuseReportRows = () => wrapper.findAllComponents(AbuseReportRow);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(AbuseReportsApp, {
+ propsData: {
+ abuseReports: mockAbuseReports,
+ pagination: { currentPage: 1, perPage: 20, totalItems: mockAbuseReports.length },
+ ...props,
+ },
+ });
+ };
+
+ it('renders AbuseReportsFilteredSearchBar', () => {
+ createComponent();
+
+ expect(findFilteredSearchBar().exists()).toBe(true);
+ });
+
+ it('renders one AbuseReportRow for each abuse report', () => {
+ createComponent();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findAbuseReportRows().length).toBe(mockAbuseReports.length);
+ });
+
+ it('renders empty state when there are no reports', () => {
+ createComponent({
+ abuseReports: [],
+ pagination: { currentPage: 1, perPage: 20, totalItems: 0 },
+ });
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ describe('pagination', () => {
+ const pagination = {
+ currentPage: 1,
+ perPage: 1,
+ totalItems: mockAbuseReports.length,
+ };
+
+ it('renders GlPagination with the correct props when needed', () => {
+ createComponent({ pagination });
+
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ value: pagination.currentPage,
+ perPage: pagination.perPage,
+ totalItems: pagination.totalItems,
+ prevText: 'Prev',
+ nextText: 'Next',
+ labelNextPage: 'Go to next page',
+ labelPrevPage: 'Go to previous page',
+ align: 'center',
+ });
+ });
+
+ it('does not render GlPagination when not needed', () => {
+ createComponent({ pagination: { currentPage: 1, perPage: 2, totalItems: 2 } });
+
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ describe('linkGen prop', () => {
+ const existingQuery = {
+ user: 'mr_okay',
+ status: 'closed',
+ };
+ const expectedGeneratedQuery = {
+ ...existingQuery,
+ page: '2',
+ };
+
+ beforeEach(() => {
+ setWindowLocation(`https://localhost?${objectToQuery(existingQuery)}`);
+ });
+
+ it('generates the correct page URL', () => {
+ createComponent({ pagination });
+
+ const linkGen = findPagination().props('linkGen');
+ const generatedUrl = linkGen(expectedGeneratedQuery.page);
+ const [, generatedQuery] = generatedUrl.split('?');
+
+ expect(queryToObject(generatedQuery)).toMatchObject(expectedGeneratedQuery);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
new file mode 100644
index 00000000000..778f055eb82
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -0,0 +1,14 @@
+export const mockAbuseReports = [
+ {
+ category: 'spam',
+ updatedAt: '2022-12-07T06:45:39.977Z',
+ reporter: { name: 'Ms. Admin' },
+ reportedUser: { name: 'Mr. Abuser' },
+ },
+ {
+ category: 'phishing',
+ updatedAt: '2022-12-07T06:45:39.977Z',
+ reporter: { name: 'Ms. Reporter' },
+ reportedUser: { name: 'Mr. Phisher' },
+ },
+];
diff --git a/spec/frontend/admin/abuse_reports/utils_spec.js b/spec/frontend/admin/abuse_reports/utils_spec.js
new file mode 100644
index 00000000000..17f0b9acb26
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/utils_spec.js
@@ -0,0 +1,13 @@
+import { FILTERED_SEARCH_TOKEN_CATEGORY } from '~/admin/abuse_reports/constants';
+import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils';
+
+describe('buildFilteredSearchCategoryToken', () => {
+ it('adds correctly formatted options to FILTERED_SEARCH_TOKEN_CATEGORY', () => {
+ const categories = ['tuxedo', 'tabby'];
+
+ expect(buildFilteredSearchCategoryToken(categories)).toMatchObject({
+ ...FILTERED_SEARCH_TOKEN_CATEGORY,
+ options: categories.map((c) => ({ value: c, title: c })),
+ });
+ });
+});
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
index c9a899ab78b..06f9ffeffcd 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
@@ -19,10 +19,6 @@ describe('DevopsScoreCallout', () => {
const findBanner = () => wrapper.findComponent(GlBanner);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with no cookie set', () => {
beforeEach(() => {
utils.setCookie = jest.fn();
diff --git a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
index 2db997942a7..969844f981c 100644
--- a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
+++ b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
@@ -29,10 +29,6 @@ describe('Form component', () => {
wrapper = mountFn(SettingsForm, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Enable inactive project deletion', () => {
it('has the checkbox', () => {
createComponent();
diff --git a/spec/frontend/admin/application_settings/network_outbound_spec.js b/spec/frontend/admin/application_settings/network_outbound_spec.js
new file mode 100644
index 00000000000..2c06a3fd67f
--- /dev/null
+++ b/spec/frontend/admin/application_settings/network_outbound_spec.js
@@ -0,0 +1,70 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+import initNetworkOutbound from '~/admin/application_settings/network_outbound';
+
+describe('initNetworkOutbound', () => {
+ const findAllowCheckboxes = () => document.querySelectorAll('.js-allow-local-requests');
+ const findDenyCheckbox = () => document.querySelector('.js-deny-all-requests');
+ const findWarningBanner = () => document.querySelector('.js-deny-all-requests-warning');
+ const clickDenyCheckbox = () => {
+ findDenyCheckbox().click();
+ };
+
+ const createFixture = (denyAll = false) => {
+ setHTMLFixture(`
+ <input class="js-deny-all-requests" type="checkbox" name="application_setting[deny_all_requests_except_allowed]" ${
+ denyAll ? 'checked="checked"' : ''
+ }/>
+ <div class="js-deny-all-requests-warning ${denyAll ? '' : 'gl-display-none'}"></div>
+ <input class="js-allow-local-requests" type="checkbox" name="application_setting[allow_local_requests_from_web_hooks_and_services]" />
+ <input class="js-allow-local-requests" type="checkbox" name="application_setting[allow_local_requests_from_system_hooks]" />
+ `);
+ };
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('when the checkbox is not checked', () => {
+ beforeEach(() => {
+ createFixture();
+ initNetworkOutbound();
+ });
+
+ it('shows banner and disables allow checkboxes on change', () => {
+ expect(findDenyCheckbox().checked).toBe(false);
+ expect(findWarningBanner().classList).toContain('gl-display-none');
+
+ clickDenyCheckbox();
+
+ expect(findDenyCheckbox().checked).toBe(true);
+ expect(findWarningBanner().classList).not.toContain('gl-display-none');
+ const allowCheckboxes = findAllowCheckboxes();
+ allowCheckboxes.forEach((checkbox) => {
+ expect(checkbox.checked).toBe(false);
+ expect(checkbox.disabled).toBe(true);
+ });
+ });
+ });
+
+ describe('when the checkbox is checked', () => {
+ beforeEach(() => {
+ createFixture(true);
+ initNetworkOutbound();
+ });
+
+ it('hides banner and enables allow checkboxes on change', () => {
+ expect(findDenyCheckbox().checked).toBe(true);
+ expect(findWarningBanner().classList).not.toContain('gl-display-none');
+
+ clickDenyCheckbox();
+
+ expect(findDenyCheckbox().checked).toBe(false);
+ expect(findWarningBanner().classList).toContain('gl-display-none');
+ const allowCheckboxes = findAllowCheckboxes();
+ allowCheckboxes.forEach((checkbox) => {
+ expect(checkbox.disabled).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js
index 1a400a101b5..315c38a2bbc 100644
--- a/spec/frontend/admin/applications/components/delete_application_spec.js
+++ b/spec/frontend/admin/applications/components/delete_application_spec.js
@@ -31,7 +31,6 @@ describe('DeleteApplication', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
index 212f4c0842c..d7b319a3d5e 100644
--- a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
+++ b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
@@ -26,10 +26,6 @@ describe('BackgroundMigrationsDatabaseListbox', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
describe('template always', () => {
diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js
index d69bf4a22bf..50d8eeb563d 100644
--- a/spec/frontend/admin/broadcast_messages/components/base_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js
@@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -12,7 +12,7 @@ import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vu
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
import { generateMockMessages, MOCK_MESSAGES } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('BroadcastMessagesBase', () => {
@@ -41,7 +41,6 @@ describe('BroadcastMessagesBase', () => {
afterEach(() => {
axiosMock.restore();
- wrapper.destroy();
});
it('renders the table and pagination when there are existing messages', () => {
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
index 36c0ac303ba..292575c984b 100644
--- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { GlBroadcastMessage, GlForm } from '@gitlab/ui';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
@@ -15,7 +15,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('MessageForm', () => {
let wrapper;
diff --git a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
index 349fab03853..432bfefeb18 100644
--- a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
@@ -21,10 +21,6 @@ describe('MessagesTable', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a table row for each message', () => {
createComponent();
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
index 4d4a2caedde..a05654a1d25 100644
--- a/spec/frontend/admin/deploy_keys/components/table_spec.js
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -9,10 +9,10 @@ import { stubComponent } from 'helpers/stub_component';
import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Api, { DEFAULT_PER_PAGE } from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
jest.mock('~/api');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('DeployKeysTable', () => {
@@ -91,10 +91,6 @@ describe('DeployKeysTable', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders page title', () => {
createComponent();
@@ -242,7 +238,7 @@ describe('DeployKeysTable', () => {
itRendersTheEmptyState();
- it('displays flash', () => {
+ it('displays alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: DeployKeysTable.i18n.apiErrorMessage,
captureError: true,
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
index eecc21e206b..9e55716cc30 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
@@ -28,10 +28,6 @@ describe('Signup Form', () => {
const findCheckboxLabel = () => findByTestId('label');
const findHelpText = () => findByTestId('helpText');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Signup Checkbox', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index f2a951bcc76..9192fc12401 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -40,8 +40,6 @@ describe('Signup Form', () => {
const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
- wrapper.destroy();
-
formSubmitSpy = null;
});
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
index 4c362a31068..60e46cddd7e 100644
--- a/spec/frontend/admin/statistics_panel/components/app_spec.js
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -30,10 +30,6 @@ describe('Admin statistics app', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findStats = (idx) => wrapper.findAll('.js-stats').at(idx);
describe('template', () => {
diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js
index 97d257c682c..c069203d046 100644
--- a/spec/frontend/admin/topics/components/remove_avatar_spec.js
+++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js
@@ -20,7 +20,7 @@ describe('RemoveAvatar', () => {
name,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlSprintf,
@@ -36,10 +36,6 @@ describe('RemoveAvatar', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('the button component', () => {
it('displays the remove button', () => {
const button = findButton();
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index 738cbd88c4c..113a0e3d404 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -59,7 +59,6 @@ describe('TopicSelect', () => {
}
afterEach(() => {
- wrapper.destroy();
jest.clearAllMocks();
});
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 8e9652332c1..4aeaa5356b4 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -22,11 +22,6 @@ describe('Action components', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('CONFIRMATION_ACTIONS', () => {
it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', (action) => {
initComponent({
diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js
index 913732aae42..d40089edc82 100644
--- a/spec/frontend/admin/users/components/app_spec.js
+++ b/spec/frontend/admin/users/components/app_spec.js
@@ -17,11 +17,6 @@ describe('AdminUsersApp component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when initialized', () => {
beforeEach(() => {
initComponent();
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 2e892e292d7..efb951f4ad2 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -73,11 +73,6 @@ describe('Delete user modal', () => {
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders modal with form included', () => {
createComponent();
expect(findForm().element).toMatchSnapshot();
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index 1b080b05c95..1a2cc3e5c34 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -32,16 +32,11 @@ describe('AdminUserActions component', () => {
showButtonLabels,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('edit button', () => {
describe('when the user has an edit action attached', () => {
beforeEach(() => {
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js
index 94fac875fbe..02e648d2b77 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/admin/users/components/user_avatar_spec.js
@@ -26,7 +26,7 @@ describe('AdminUserAvatar component', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
GlAvatarLabeled,
@@ -34,11 +34,6 @@ describe('AdminUserAvatar component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when initialized', () => {
beforeEach(() => {
initComponent();
diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js
index 73be33d5a9d..19c1cd38a50 100644
--- a/spec/frontend/admin/users/components/user_date_spec.js
+++ b/spec/frontend/admin/users/components/user_date_spec.js
@@ -17,11 +17,6 @@ describe('FormatDate component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
date | dateFormat | output
${mockDate} | ${undefined} | ${'Nov 13, 2020'}
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index a0aec347b6b..6f658fd2e59 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -10,12 +10,12 @@ import AdminUserActions from '~/admin/users/components/user_actions.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import AdminUserDate from '~/vue_shared/components/user_date.vue';
import { users, paths, createGroupCountResponse } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -57,11 +57,6 @@ describe('AdminUsersTable component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when there are users', () => {
beforeEach(() => {
initComponent();
@@ -134,7 +129,7 @@ describe('AdminUsersTable component', () => {
await waitForPromises();
});
- it('creates a flash message and captures the error', () => {
+ it('creates an alert message and captures the error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Could not load user group counts. Please refresh the page to try again.',
captureError: true,
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index b51858d5129..d8a94ee5e1d 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -19,8 +19,6 @@ describe('initAdminUsersApp', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
el = null;
});
@@ -47,8 +45,6 @@ describe('initAdminUserActions', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
el = null;
});
diff --git a/spec/frontend/airflow/dags/components/dags_spec.js b/spec/frontend/airflow/dags/components/dags_spec.js
deleted file mode 100644
index f9cf4fc87af..00000000000
--- a/spec/frontend/airflow/dags/components/dags_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { GlAlert, GlPagination, GlTableLite } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import AirflowDags from '~/airflow/dags/components/dags.vue';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import { mockDags } from './mock_data';
-
-describe('AirflowDags', () => {
- let wrapper;
-
- const createWrapper = (
- dags = [],
- pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
- ) => {
- wrapper = mountExtended(AirflowDags, {
- propsData: {
- dags,
- pagination,
- },
- });
- };
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findEmptyState = () => wrapper.findByText('There are no DAGs to show');
- const findPagination = () => wrapper.findComponent(GlPagination);
-
- describe('default (no dags)', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('shows incubation warning', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('shows empty state', () => {
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it('does not show pagination', () => {
- expect(findPagination().exists()).toBe(false);
- });
- });
-
- describe('with dags', () => {
- const createWrapperWithDags = (pagination = {}) => {
- createWrapper(mockDags, {
- page: 1,
- isLastPage: false,
- per_page: 2,
- totalItems: 5,
- ...pagination,
- });
- };
-
- const findDagsData = () => {
- return wrapper
- .findComponent(GlTableLite)
- .findAll('tbody tr')
- .wrappers.map((tr) => {
- return tr.findAll('td').wrappers.map((td) => {
- const timeAgo = td.findComponent(TimeAgo);
-
- if (timeAgo.exists()) {
- return {
- type: 'time',
- value: timeAgo.props('time'),
- };
- }
-
- return {
- type: 'text',
- value: td.text(),
- };
- });
- });
- };
-
- it('renders the table of Dags with data', () => {
- createWrapperWithDags();
-
- expect(findDagsData()).toEqual(
- mockDags.map((x) => [
- { type: 'text', value: x.dag_name },
- { type: 'text', value: x.schedule },
- { type: 'time', value: x.next_run },
- { type: 'text', value: String(x.is_active) },
- { type: 'text', value: String(x.is_paused) },
- { type: 'text', value: x.fileloc },
- ]),
- );
- });
-
- describe('Pagination behaviour', () => {
- it.each`
- pagination | expected
- ${{}} | ${{ value: 1, prevPage: null, nextPage: 2 }}
- ${{ page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: 3 }}
- ${{ isLastPage: true, page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: null }}
- `('with $pagination, sets pagination props', ({ pagination, expected }) => {
- createWrapperWithDags({ ...pagination });
-
- expect(findPagination().props()).toMatchObject(expected);
- });
-
- it('generates link for each page', () => {
- createWrapperWithDags();
-
- const generateLink = findPagination().props('linkGen');
-
- expect(generateLink(3)).toBe(`${TEST_HOST}/?page=3`);
- });
- });
- });
-});
diff --git a/spec/frontend/airflow/dags/components/mock_data.js b/spec/frontend/airflow/dags/components/mock_data.js
deleted file mode 100644
index 9547282517d..00000000000
--- a/spec/frontend/airflow/dags/components/mock_data.js
+++ /dev/null
@@ -1,67 +0,0 @@
-export const mockDags = [
- {
- id: 1,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 1',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 2,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 2',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 3,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 3',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 4,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 4',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 5,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 5',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
-];
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 7fb4f2d2463..3f709d8c9f5 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -68,7 +68,7 @@ describe('AlertManagementTable', () => {
},
stubs,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
}),
);
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/alert_spec.js
index 17d6cea23df..1ae8373016b 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/alert_spec.js
@@ -1,6 +1,6 @@
import * as Sentry from '@sentry/browser';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
jest.mock('@sentry/browser');
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index 0e402e61bcc..4a60d605cae 100644
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -51,36 +51,23 @@ exports[`Alert integration settings form default state should match the default
</gl-link-stub>
</label>
- <gl-dropdown-stub
+ <gl-collapsible-listbox-stub
block="true"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
data-qa-selector="incident_templates_dropdown"
headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
id="alert-integration-settings-issue-template"
+ items="[object Object]"
+ noresultstext="No results found"
+ placement="left"
+ resetbuttonlabel=""
+ searchplaceholder="Search"
+ selected="selecte_tmpl"
size="medium"
- text="selecte_tmpl"
+ toggletext=""
variant="default"
- >
- <gl-dropdown-item-stub
- avatarurl=""
- data-qa-selector="incident_templates_item"
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- >
-
- No template selected
-
- </gl-dropdown-item-stub>
- </gl-dropdown-stub>
+ />
</gl-form-group-stub>
<gl-form-group-stub
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 a15c78cc456..67d8619f157 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -30,7 +30,7 @@ import {
INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
@@ -48,7 +48,7 @@ import {
} from './mocks/apollo_mock';
import mockIntegrations from './mocks/integrations.json';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('AlertsSettingsWrapper', () => {
let wrapper;
@@ -128,10 +128,6 @@ describe('AlertsSettingsWrapper', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent({
@@ -478,7 +474,7 @@ describe('AlertsSettingsWrapper', () => {
expect(destroyIntegrationHandler).toHaveBeenCalled();
});
- it('displays flash if mutation had a recoverable error', async () => {
+ it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors),
});
@@ -489,7 +485,7 @@ describe('AlertsSettingsWrapper', () => {
expect(createAlert).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
- it('displays flash if mutation had a non-recoverable error', async () => {
+ it('displays alert if mutation had a non-recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockRejectedValue('Error'),
});
diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/components/activity_chart_spec.js
index c26407f5c1d..4f8126aaacf 100644
--- a/spec/frontend/analytics/components/activity_chart_spec.js
+++ b/spec/frontend/analytics/components/activity_chart_spec.js
@@ -13,11 +13,6 @@ describe('Activity Chart Bundle', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findChart = () => wrapper.findComponent(GlColumnChart);
const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
diff --git a/spec/frontend/analytics/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/base_spec.js
index 58588ff49ce..033916eabcd 100644
--- a/spec/frontend/analytics/cycle_analytics/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/base_spec.js
@@ -31,13 +31,15 @@ Vue.use(Vuex);
let wrapper;
-const { id: groupId, path: groupPath } = currentGroup;
+const { path } = currentGroup;
+const groupPath = `groups/${path}`;
const defaultState = {
currentGroup,
createdBefore,
createdAfter,
stageCounts,
- endpoints: { fullPath, groupId, groupPath },
+ groupPath,
+ namespace: { fullPath },
};
function createStore({ initialState = {}, initialGetters = {} }) {
@@ -93,11 +95,6 @@ describe('Value stream analytics component', () => {
wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents, pagination } });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders the path navigation component', () => {
expect(findPathNavigation().exists()).toBe(true);
});
@@ -139,7 +136,6 @@ describe('Value stream analytics component', () => {
it('passes the paths to the filter bar', () => {
expect(findFilters().props()).toEqual({
- groupId,
groupPath,
endDate: createdBefore,
hasDateRangeFilter: true,
@@ -157,6 +153,10 @@ describe('Value stream analytics component', () => {
expect(findPagination().exists()).toBe(true);
});
+ it('does not render a link to the value streams dashboard', () => {
+ expect(findOverviewMetrics().props('dashboardsPath')).toBeNull();
+ });
+
describe('with `cycleAnalyticsForGroups=true` license', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } });
@@ -167,6 +167,23 @@ describe('Value stream analytics component', () => {
});
});
+ describe('with `groupAnalyticsDashboardsPage=true` and `groupLevelAnalyticsDashboard=true` license', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: {
+ features: { groupAnalyticsDashboardsPage: true, groupLevelAnalyticsDashboard: true },
+ },
+ });
+ });
+
+ it('renders a link to the value streams dashboard', () => {
+ expect(findOverviewMetrics().props('dashboardsPath')).toBeDefined();
+ expect(findOverviewMetrics().props('dashboardsPath')).toBe(
+ '/groups/foo/-/analytics/dashboards/value_streams_dashboard?query=full/path/to/foo',
+ );
+ });
+ });
+
describe('isLoading = true', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
index 2b26b202882..da7824adbf9 100644
--- a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
@@ -98,7 +98,6 @@ describe('Filter bar', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
index 9be92bb92bc..6dd7e2e6223 100644
--- a/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
@@ -16,10 +16,6 @@ describe('Formatted Stage Count', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
stageCount | expectedOutput
${null} | ${'-'}
diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index f820f755400..216e07844b8 100644
--- a/spec/frontend/analytics/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -219,6 +219,8 @@ export const group = {
};
export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true });
+export const groupNamespace = { id: currentGroup.id, fullPath: `groups/${currentGroup.path}` };
+export const projectNamespace = { fullPath: 'some/cool/path' };
export const selectedProjects = [
{
diff --git a/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
index 107e62035c3..9a598ee0ad1 100644
--- a/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
@@ -50,8 +50,6 @@ describe('Project PathNavigation', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
- wrapper = null;
});
describe('displays correctly', () => {
diff --git a/spec/frontend/analytics/cycle_analytics/stage_table_spec.js b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
index cfccce7eae9..fbc63a80de8 100644
--- a/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
@@ -51,10 +51,6 @@ function createComponent(props = {}, shallow = false) {
}
describe('StageTable', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('is loaded with data', () => {
beforeEach(() => {
wrapper = createComponent();
@@ -258,7 +254,6 @@ describe('StageTable', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
it('will display the pagination component', () => {
@@ -305,7 +300,6 @@ describe('StageTable', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
it('can sort the end event or duration', () => {
diff --git a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index 3030fca126b..b2ce8596c22 100644
--- a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -13,21 +13,13 @@ import {
createdBefore,
initialPaginationState,
reviewEvents,
+ projectNamespace as namespace,
} from '../mock_data';
-const { id: groupId, path: groupPath } = currentGroup;
-const mockMilestonesPath = 'mock-milestones.json';
-const mockLabelsPath = 'mock-labels.json';
-const mockRequestPath = 'some/cool/path';
+const { path: groupPath } = currentGroup;
+const mockMilestonesPath = `/${namespace.fullPath}/-/milestones.json`;
+const mockLabelsPath = `/${namespace.fullPath}/-/labels.json`;
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
-const mockEndpoints = {
- fullPath: mockFullPath,
- requestPath: mockRequestPath,
- labelsPath: mockLabelsPath,
- milestonesPath: mockMilestonesPath,
- groupId,
- groupPath,
-};
const mockSetDateActionCommit = {
payload: { createdAfter, createdBefore },
type: 'SET_DATE_RANGE',
@@ -35,6 +27,7 @@ const mockSetDateActionCommit = {
const defaultState = {
...getters,
+ namespace,
selectedValueStream,
createdAfter,
createdBefore,
@@ -81,7 +74,8 @@ describe('Project Value Stream Analytics actions', () => {
const selectedAssigneeList = ['Assignee 1', 'Assignee 2'];
const selectedLabelList = ['Label 1', 'Label 2'];
const payload = {
- endpoints: mockEndpoints,
+ namespace,
+ groupPath,
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
@@ -92,7 +86,7 @@ describe('Project Value Stream Analytics actions', () => {
groupEndpoint: 'foo',
labelsEndpoint: mockLabelsPath,
milestonesEndpoint: mockMilestonesPath,
- projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
+ projectEndpoint: namespace.fullPath,
};
it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => {
@@ -193,7 +187,6 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
...defaultState,
- endpoints: mockEndpoints,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -219,7 +212,6 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
...defaultState,
- endpoints: mockEndpoints,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -243,7 +235,6 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
...defaultState,
- endpoints: mockEndpoints,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -265,9 +256,7 @@ describe('Project Value Stream Analytics actions', () => {
const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/;
beforeEach(() => {
- state = {
- endpoints: mockEndpoints,
- };
+ state = { namespace };
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_OK);
});
@@ -333,7 +322,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
- endpoints: mockEndpoints,
+ namespace,
selectedValueStream,
};
mock = new MockAdapter(axios);
diff --git a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 567fac81e1f..70b7454f4a0 100644
--- a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -17,12 +17,14 @@ import {
rawStageCounts,
stageCounts,
initialPaginationState as pagination,
+ projectNamespace as mockNamespace,
} from '../mock_data';
let state;
const rawEvents = rawIssueEvents.events;
const convertedEvents = issueEvents.events;
-const mockRequestPath = 'fake/request/path';
+const mockGroupPath = 'groups/path';
+const mockFeatures = { some: 'feature' };
const mockCreatedAfter = '2020-06-18';
const mockCreatedBefore = '2020-07-18';
@@ -64,19 +66,22 @@ describe('Project Value Stream Analytics mutations', () => {
const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore };
const mockInitialPayload = {
- endpoints: { requestPath: mockRequestPath },
currentGroup: { title: 'cool-group' },
id: 1337,
+ groupPath: mockGroupPath,
+ namespace: mockNamespace,
+ features: mockFeatures,
...mockSetDatePayload,
};
const mockInitializedObj = {
- endpoints: { requestPath: mockRequestPath },
...mockSetDatePayload,
};
it.each`
mutation | stateKey | value
- ${types.INITIALIZE_VSA} | ${'endpoints'} | ${{ requestPath: mockRequestPath }}
+ ${types.INITIALIZE_VSA} | ${'features'} | ${mockFeatures}
+ ${types.INITIALIZE_VSA} | ${'namespace'} | ${mockNamespace}
+ ${types.INITIALIZE_VSA} | ${'groupPath'} | ${mockGroupPath}
${types.INITIALIZE_VSA} | ${'createdAfter'} | ${mockCreatedAfter}
${types.INITIALIZE_VSA} | ${'createdBefore'} | ${mockCreatedBefore}
`('$mutation will set $stateKey', ({ mutation, stateKey, value }) => {
diff --git a/spec/frontend/analytics/cycle_analytics/total_time_spec.js b/spec/frontend/analytics/cycle_analytics/total_time_spec.js
index 47ee7aad8c4..6597b6fa3d5 100644
--- a/spec/frontend/analytics/cycle_analytics/total_time_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/total_time_spec.js
@@ -10,10 +10,6 @@ describe('TotalTime', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with a valid time object', () => {
it.each`
time
diff --git a/spec/frontend/analytics/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js
index fe412bf7498..e6d17edcadc 100644
--- a/spec/frontend/analytics/cycle_analytics/utils_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js
@@ -91,9 +91,9 @@ describe('Value stream analytics utils', () => {
const projectId = '5';
const createdAfter = '2021-09-01';
const createdBefore = '2021-11-06';
- const groupId = '146';
const groupPath = 'fake-group';
- const fullPath = 'fake-group/fake-project';
+ const namespaceName = 'Fake project';
+ const namespaceFullPath = 'fake-group/fake-project';
const labelsPath = '/fake-group/fake-project/-/labels.json';
const milestonesPath = '/fake-group/fake-project/-/milestones.json';
const requestPath = '/fake-group/fake-project/-/value_stream_analytics';
@@ -102,11 +102,11 @@ describe('Value stream analytics utils', () => {
projectId,
createdBefore,
createdAfter,
- fullPath,
+ namespaceName,
+ namespaceFullPath,
requestPath,
labelsPath,
milestonesPath,
- groupId,
groupPath,
};
@@ -124,14 +124,13 @@ describe('Value stream analytics utils', () => {
expect(res.createdAfter).toEqual(new Date(createdAfter));
});
+ it('sets the namespace', () => {
+ expect(res.namespace.name).toBe(namespaceName);
+ expect(res.namespace.fullPath).toBe(namespaceFullPath);
+ });
+
it('sets the endpoints', () => {
- const { endpoints } = res;
- expect(endpoints.fullPath).toBe(fullPath);
- expect(endpoints.requestPath).toBe(requestPath);
- expect(endpoints.labelsPath).toBe(labelsPath);
- expect(endpoints.milestonesPath).toBe(milestonesPath);
- expect(endpoints.groupId).toBe(parseInt(groupId, 10));
- expect(endpoints.groupPath).toBe(groupPath);
+ expect(res.groupPath).toBe(`groups/${groupPath}`);
});
it('returns null when there is no stage', () => {
@@ -164,7 +163,7 @@ describe('Value stream analytics utils', () => {
...rawData,
gon: { licensed_features: fakeFeatures },
});
- expect(res.features).toEqual(fakeFeatures);
+ expect(res.features).toMatchObject(fakeFeatures);
});
});
});
diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
index 4f333e95d89..160f6ce0563 100644
--- a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
@@ -34,11 +34,6 @@ describe('ValueStreamFilters', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('will render the filter bar', () => {
expect(findFilterBar().exists()).toBe(true);
});
diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
index 948dc5c9be2..6a64737bc80 100644
--- a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
@@ -8,10 +8,11 @@ import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
-import { createAlert } from '~/flash';
+import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
+import { createAlert } from '~/alert';
import { group } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ValueStreamMetrics', () => {
let wrapper;
@@ -37,6 +38,7 @@ describe('ValueStreamMetrics', () => {
});
};
+ const findVSDLink = () => wrapper.findComponent(ValueStreamsDashboardLink);
const findMetrics = () => wrapper.findAllComponents(MetricTile);
const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
@@ -48,10 +50,6 @@ describe('ValueStreamMetrics', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with successful requests', () => {
beforeEach(() => {
mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
@@ -168,6 +166,25 @@ describe('ValueStreamMetrics', () => {
});
});
+ describe('Value Streams Dashboard Link', () => {
+ it('will render when a dashboardsPath is set', async () => {
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS, dashboardsPath: 'fake-group-path' });
+ await waitForPromises();
+
+ const vsdLink = findVSDLink();
+
+ expect(vsdLink.exists()).toBe(true);
+ expect(vsdLink.props()).toEqual({ requestPath: 'fake-group-path' });
+ });
+
+ it('does not render without a dashboardsPath', async () => {
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
+ await waitForPromises();
+
+ expect(findVSDLink().exists()).toBe(false);
+ });
+ });
+
describe('with a request failing', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue();
diff --git a/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
index c62bfb11f7b..70bfce41c82 100644
--- a/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
+++ b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
@@ -6,10 +6,6 @@ import ServicePingDisabled from '~/analytics/devops_reports/components/service_p
describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const createWrapper = ({ isAdmin = false } = {}) => {
wrapper = mountExtended(ServicePingDisabled, {
provide: {
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index 562e86529ee..5f0847e0db6 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -22,10 +22,6 @@ describe('Daterange component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker);
const findDateRangeIndicator = () => wrapper.findByTestId('daterange-picker-indicator');
@@ -90,18 +86,19 @@ describe('Daterange component', () => {
});
describe('set', () => {
- it('emits the change event with an object containing startDate and endDate', () => {
+ it('emits the change event with an object containing startDate and endDate', async () => {
const startDate = new Date('2019-10-01');
const endDate = new Date('2019-10-05');
- wrapper.vm.dateRange = { startDate, endDate };
- expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]);
+ await findDaterangePicker().vm.$emit('input', { startDate, endDate });
+
+ expect(wrapper.emitted('change')).toEqual([[{ startDate, endDate }]]);
});
});
describe('get', () => {
- it("returns value of dateRange from state's startDate and endDate", () => {
- expect(wrapper.vm.dateRange).toEqual({
+ it("datepicker to have default of dateRange from state's startDate and endDate", () => {
+ expect(findDaterangePicker().props('value')).toEqual({
startDate: defaultProps.startDate,
endDate: defaultProps.endDate,
});
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index e0bfff3e664..d7e6606cdc6 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -34,10 +34,6 @@ describe('MetricPopover', () => {
const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);
const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the metric label', () => {
wrapper = createComponent({ metric: MOCK_METRIC });
expect(findMetricLabel().text()).toBe(MOCK_METRIC.label);
diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js
index 980dfad9eb0..00e82cff0f0 100644
--- a/spec/frontend/analytics/shared/components/metric_tile_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js
@@ -21,10 +21,6 @@ describe('MetricTile', () => {
const findSingleStat = () => wrapper.findComponent(GlSingleStat);
const findPopover = () => wrapper.findComponent(MetricPopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe('links', () => {
it('when the metric has links, it redirects the user on click', () => {
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 3871fd530d8..d2cbe0d39e4 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -70,10 +70,6 @@ describe('ProjectsDropdownFilter component', () => {
return waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items');
const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items');
const findClearAllButton = () => wrapper.findByText('Clear all');
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
index b48e2d971b5..24af7b836d5 100644
--- a/spec/frontend/analytics/shared/utils_spec.js
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -5,6 +5,7 @@ import {
extractPaginationQueryParameters,
getDataZoomOption,
prepareTimeMetricsData,
+ generateValueStreamsDashboardLink,
} from '~/analytics/shared/utils';
import { slugify } from '~/lib/utils/text_utility';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -212,3 +213,30 @@ describe('prepareTimeMetricsData', () => {
]);
});
});
+
+describe('generateValueStreamsDashboardLink', () => {
+ it.each`
+ groupPath | projectPaths | result
+ ${''} | ${[]} | ${''}
+ ${'groups/fake-group'} | ${[]} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard'}
+ ${'groups/fake-group'} | ${['fake-path/project_1']} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard?query=fake-path/project_1'}
+ ${'groups/fake-group'} | ${['fake-path/project_1', 'fake-path/project_2']} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard?query=fake-path/project_1,fake-path/project_2'}
+ `(
+ 'generates the dashboard link when groupPath=$groupPath and projectPaths=$projectPaths',
+ ({ groupPath, projectPaths, result }) => {
+ expect(generateValueStreamsDashboardLink(groupPath, projectPaths)).toBe(result);
+ },
+ );
+
+ describe('with a relative url rool set', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('with includes a relative path if one is set', () => {
+ expect(generateValueStreamsDashboardLink('groups/fake-path', ['project_1'])).toBe(
+ '/foobar/groups/fake-path/-/analytics/dashboards/value_streams_dashboard?query=project_1',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/analytics/usage_trends/components/app_spec.js b/spec/frontend/analytics/usage_trends/components/app_spec.js
index c732dc22322..f9338661ebf 100644
--- a/spec/frontend/analytics/usage_trends/components/app_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/app_spec.js
@@ -15,11 +15,6 @@ describe('UsageTrendsApp', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays the usage counts component', () => {
expect(wrapper.findComponent(UsageCounts).exists()).toBe(true);
});
diff --git a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
index f4cbc56be5c..a71ce090955 100644
--- a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
@@ -26,10 +26,6 @@ describe('UsageCounts', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
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 ad6089f74b5..322d05e663a 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
@@ -45,11 +45,6 @@ describe('UsageTrendsCountChart', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
const findChart = () => wrapper.findComponent(GlLineChart);
const findAlert = () => wrapper.findComponent(GlAlert);
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 e7abd4d4323..20836d7cc70 100644
--- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
@@ -42,11 +42,6 @@ describe('UsersChart', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
const findAlert = () => wrapper.findComponent(GlAlert);
const findChart = () => wrapper.findComponent(GlAreaChart);
diff --git a/spec/frontend/api/alert_management_alerts_api_spec.js b/spec/frontend/api/alert_management_alerts_api_spec.js
index 507f659a170..86052a05b76 100644
--- a/spec/frontend/api/alert_management_alerts_api_spec.js
+++ b/spec/frontend/api/alert_management_alerts_api_spec.js
@@ -9,7 +9,6 @@ import {
describe('~/api/alert_management_alerts_api.js', () => {
let mock;
- let originalGon;
const projectId = 1;
const alertIid = 2;
@@ -19,13 +18,11 @@ describe('~/api/alert_management_alerts_api.js', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v4' };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('fetchAlertMetricImages', () => {
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index 0315db02cf2..642edb33624 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -10,23 +10,18 @@ const mockUrlRoot = '/gitlab';
const mockGroupId = '99';
describe('GroupsApi', () => {
- let originalGon;
let mock;
- const dummyGon = {
- api_version: mockApiVersion,
- relative_url_root: mockUrlRoot,
- };
-
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: mockApiVersion,
+ relative_url_root: mockUrlRoot,
+ };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('updateGroup', () => {
diff --git a/spec/frontend/api/packages_api_spec.js b/spec/frontend/api/packages_api_spec.js
index 5f517bcf358..37c4b926ec2 100644
--- a/spec/frontend/api/packages_api_spec.js
+++ b/spec/frontend/api/packages_api_spec.js
@@ -6,22 +6,18 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Api', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
- const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
- };
- let originalGon;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('packages', () => {
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 2d4ed39dad0..2de56fae0c2 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -7,7 +7,6 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/projects_api.js', () => {
let mock;
- let originalGon;
const projectId = 1;
const setfullPathProjectSearch = (value) => {
@@ -17,13 +16,11 @@ describe('~/api/projects_api.js', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('getProjects', () => {
diff --git a/spec/frontend/api/tags_api_spec.js b/spec/frontend/api/tags_api_spec.js
index af3533f52b7..0a1177d4f60 100644
--- a/spec/frontend/api/tags_api_spec.js
+++ b/spec/frontend/api/tags_api_spec.js
@@ -5,20 +5,17 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/tags_api.js', () => {
let mock;
- let originalGon;
const projectId = 1;
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v7' };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('getTag', () => {
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index 4d0252aad23..6636d77a09b 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -12,19 +12,16 @@ import { timeRanges } from '~/vue_shared/constants';
describe('~/api/user_api', () => {
let axiosMock;
- let originalGon;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v4' };
});
afterEach(() => {
axiosMock.restore();
axiosMock.resetHistory();
- window.gon = originalGon;
});
describe('followUser', () => {
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 6fd106502c4..4ef37311e51 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -10,27 +10,22 @@ import {
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
-jest.mock('~/flash');
-
describe('Api', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
- const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
- };
- let originalGon;
+
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('buildUrl', () => {
@@ -1423,7 +1418,7 @@ describe('Api', () => {
describe('when service data increment counter is called with feature flag disabled', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: false };
+ gon.features = { usageDataApi: false };
});
it('returns null', () => {
@@ -1437,7 +1432,7 @@ describe('Api', () => {
describe('when service data increment counter is called', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: true };
+ gon.features = { usageDataApi: true };
});
it('resolves the Promise', () => {
@@ -1468,7 +1463,7 @@ describe('Api', () => {
describe('when service data increment unique users is called with feature flag disabled', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: false };
+ gon.features = { usageDataApi: false };
});
it('returns null and does not call the endpoint', () => {
@@ -1483,7 +1478,7 @@ describe('Api', () => {
describe('when service data increment unique users is called', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: true };
+ gon.features = { usageDataApi: true };
});
it('resolves the Promise', () => {
@@ -1500,7 +1495,7 @@ describe('Api', () => {
describe('when user is not set and feature flag enabled', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: true };
+ gon.features = { usageDataApi: true };
});
it('returns null and does not call the endpoint', () => {
diff --git a/spec/frontend/approvals/mock_data.js b/spec/frontend/approvals/mock_data.js
new file mode 100644
index 00000000000..e0e90c09791
--- /dev/null
+++ b/spec/frontend/approvals/mock_data.js
@@ -0,0 +1,10 @@
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
+
+export const createCanApproveResponse = () => {
+ const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
+ response.data.project.mergeRequest.userPermissions.canApprove = true;
+ response.data.project.mergeRequest.approved = false;
+ response.data.project.mergeRequest.approvedBy.nodes = [];
+
+ return response;
+};
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js
index 2a7156bf480..268772ed4c0 100644
--- a/spec/frontend/artifacts/components/artifact_row_spec.js
+++ b/spec/frontend/artifacts/components/artifact_row_spec.js
@@ -1,9 +1,10 @@
-import { GlBadge, GlButton, GlFriendlyWrap } from '@gitlab/ui';
+import { GlBadge, GlButton, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui';
import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ArtifactRow from '~/artifacts/components/artifact_row.vue';
+import { BULK_DELETE_FEATURE_FLAG } from '~/artifacts/constants';
describe('ArtifactRow component', () => {
let wrapper;
@@ -15,23 +16,21 @@ describe('ArtifactRow component', () => {
const findSize = () => wrapper.findByTestId('job-artifact-row-size');
const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const createComponent = ({ canDestroyArtifacts = true } = {}) => {
+ const createComponent = ({ canDestroyArtifacts = true, glFeatures = {} } = {}) => {
wrapper = shallowMountExtended(ArtifactRow, {
propsData: {
artifact,
+ isSelected: false,
isLoading: false,
isLastRow: false,
},
- provide: { canDestroyArtifacts },
+ provide: { canDestroyArtifacts, glFeatures },
stubs: { GlBadge, GlButton, GlFriendlyWrap },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('artifact details', () => {
beforeEach(async () => {
createComponent();
@@ -77,4 +76,30 @@ describe('ArtifactRow component', () => {
expect(wrapper.emitted('delete')).toBeDefined();
});
});
+
+ describe('bulk delete checkbox', () => {
+ describe('with permission and feature flag enabled', () => {
+ beforeEach(() => {
+ createComponent({ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true } });
+ });
+
+ it('emits selectArtifact when toggled', () => {
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toStrictEqual([[artifact, true]]);
+ });
+ });
+
+ it('is not shown without permission', () => {
+ createComponent({ canDestroyArtifacts: false });
+
+ expect(findCheckbox().exists()).toBe(false);
+ });
+
+ it('is not shown with feature flag disabled', () => {
+ createComponent();
+
+ expect(findCheckbox().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js
new file mode 100644
index 00000000000..876906b2c3c
--- /dev/null
+++ b/spec/frontend/artifacts/components/artifacts_bulk_delete_spec.js
@@ -0,0 +1,96 @@
+import { GlSprintf, GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue';
+import bulkDestroyArtifactsMutation from '~/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
+
+Vue.use(VueApollo);
+
+describe('ArtifactsBulkDelete component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const projectId = '123';
+ const selectedArtifacts = [
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0].id,
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[1].id,
+ ];
+
+ const findText = () => wrapper.findComponent(GlSprintf).text();
+ const findDeleteButton = () => wrapper.findByTestId('bulk-delete-delete-button');
+ const findClearButton = () => wrapper.findByTestId('bulk-delete-clear-button');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = ({
+ handlers = {
+ bulkDestroyArtifactsMutation: jest.fn(),
+ },
+ } = {}) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(ArtifactsBulkDelete, {
+ apolloProvider: createMockApollo([
+ [bulkDestroyArtifactsMutation, requestHandlers.bulkDestroyArtifactsMutation],
+ ]),
+ propsData: {
+ selectedArtifacts,
+ queryVariables: {},
+ isLoading: false,
+ isLastRow: false,
+ },
+ provide: { projectId },
+ });
+ };
+
+ describe('selected artifacts box', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('displays selected artifacts count', () => {
+ expect(findText()).toContain(String(selectedArtifacts.length));
+ });
+
+ it('opens the confirmation modal when the delete button is clicked', async () => {
+ expect(findModal().props('visible')).toBe(false);
+
+ findDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ it('emits clearSelectedArtifacts event when the clear button is clicked', () => {
+ findClearButton().trigger('click');
+
+ expect(wrapper.emitted('clearSelectedArtifacts')).toBeDefined();
+ });
+ });
+
+ describe('bulk delete confirmation modal', () => {
+ beforeEach(async () => {
+ createComponent();
+ findDeleteButton().trigger('click');
+ await waitForPromises();
+ });
+
+ it('calls the bulk delete mutation with the selected artifacts on confirm', () => {
+ findModal().vm.$emit('primary');
+
+ expect(requestHandlers.bulkDestroyArtifactsMutation).toHaveBeenCalledWith({
+ projectId: `gid://gitlab/Project/${projectId}`,
+ ids: selectedArtifacts,
+ });
+ });
+
+ it('does not call the bulk delete mutation on cancel', () => {
+ findModal().vm.$emit('cancel');
+
+ expect(requestHandlers.bulkDestroyArtifactsMutation).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
index d006e0285d2..6bf3498f9b0 100644
--- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
+++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
@@ -10,9 +10,9 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0];
const refetchArtifacts = jest.fn();
@@ -25,11 +25,12 @@ describe('ArtifactsTableRowDetails component', () => {
const findModal = () => wrapper.findComponent(GlModal);
- const createComponent = (
+ const createComponent = ({
handlers = {
destroyArtifactMutation: jest.fn(),
},
- ) => {
+ selectedArtifacts = [],
+ } = {}) => {
requestHandlers = handlers;
wrapper = mountExtended(ArtifactsTableRowDetails, {
apolloProvider: createMockApollo([
@@ -37,6 +38,7 @@ describe('ArtifactsTableRowDetails component', () => {
]),
propsData: {
artifacts,
+ selectedArtifacts,
refetchArtifacts,
queryVariables: {},
},
@@ -47,10 +49,6 @@ describe('ArtifactsTableRowDetails component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('passes correct props', () => {
beforeEach(() => {
createComponent();
@@ -92,7 +90,7 @@ describe('ArtifactsTableRowDetails component', () => {
});
});
- it('displays a flash message and refetches artifacts when the mutation fails', async () => {
+ it('displays an alert message and refetches artifacts when the mutation fails', async () => {
createComponent({
destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')),
});
@@ -120,4 +118,20 @@ describe('ArtifactsTableRowDetails component', () => {
expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled();
});
});
+
+ describe('bulk delete selection', () => {
+ it('is not selected for unselected artifact', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(false);
+ });
+
+ it('is selected for selected artifacts', async () => {
+ createComponent({ selectedArtifacts: [artifacts.nodes[0].id] });
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/artifacts/components/feedback_banner_spec.js
index 3421486020a..af9599daefa 100644
--- a/spec/frontend/artifacts/components/feedback_banner_spec.js
+++ b/spec/frontend/artifacts/components/feedback_banner_spec.js
@@ -32,10 +32,6 @@ describe('Artifacts management feedback banner', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('is displayed with the correct props', () => {
createComponent();
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
index dbe4598f599..40f3c9633ab 100644
--- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
@@ -1,4 +1,12 @@
-import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } from '@gitlab/ui';
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlBadge,
+ GlPagination,
+ GlModal,
+ GlFormCheckbox,
+} from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
@@ -8,15 +16,22 @@ import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsBulkDelete from '~/artifacts/components/artifacts_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants';
+import {
+ ARCHIVE_FILE_TYPE,
+ JOBS_PER_PAGE,
+ I18N_FETCH_ERROR,
+ INITIAL_CURRENT_PAGE,
+ BULK_DELETE_FEATURE_FLAG,
+} from '~/artifacts/constants';
import { totalArtifactsSizeForJob } from '~/artifacts/utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -24,6 +39,8 @@ describe('JobArtifactsTable component', () => {
let wrapper;
let requestHandlers;
+ const mockToastShow = jest.fn();
+
const findBanner = () => wrapper.findComponent(FeedbackBanner);
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
@@ -55,6 +72,11 @@ describe('JobArtifactsTable component', () => {
const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+ // first checkbox is a "select all", this finder should get the first job checkbox
+ const findJobCheckbox = () => wrapper.findAllComponents(GlFormCheckbox).at(1);
+ const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findBulkDelete = () => wrapper.findComponent(ArtifactsBulkDelete);
+
const findPagination = () => wrapper.findComponent(GlPagination);
const setPage = async (page) => {
findPagination().vm.$emit('input', page);
@@ -69,7 +91,14 @@ describe('JobArtifactsTable component', () => {
];
}
const getJobArtifactsResponseThatPaginates = {
- data: { project: { jobs: { nodes: enoughJobsToPaginate } } },
+ data: {
+ project: {
+ jobs: {
+ nodes: enoughJobsToPaginate,
+ pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo, hasNextPage: true },
+ },
+ },
+ },
};
const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
@@ -77,13 +106,14 @@ describe('JobArtifactsTable component', () => {
(artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
);
- const createComponent = (
+ const createComponent = ({
handlers = {
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
},
data = {},
canDestroyArtifacts = true,
- ) => {
+ glFeatures = {},
+ } = {}) => {
requestHandlers = handlers;
wrapper = mountExtended(JobArtifactsTable, {
apolloProvider: createMockApollo([
@@ -91,8 +121,15 @@ describe('JobArtifactsTable component', () => {
]),
provide: {
projectPath: 'project/path',
+ projectId: 'gid://projects/id',
canDestroyArtifacts,
artifactsManagementFeedbackImagePath: 'banner/image/path',
+ glFeatures,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
},
data() {
return data;
@@ -100,10 +137,6 @@ describe('JobArtifactsTable component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders feedback banner', () => {
createComponent();
@@ -118,7 +151,9 @@ describe('JobArtifactsTable component', () => {
it('on error, shows an alert', async () => {
createComponent({
- getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
+ },
});
await waitForPromises();
@@ -259,10 +294,10 @@ describe('JobArtifactsTable component', () => {
archive: { downloadPath: null },
};
- createComponent(
- { getJobArtifactsQuery: jest.fn() },
- { jobArtifacts: [jobWithoutDownloadPath] },
- );
+ createComponent({
+ handlers: { getJobArtifactsQuery: jest.fn() },
+ data: { jobArtifacts: [jobWithoutDownloadPath] },
+ });
await waitForPromises();
@@ -285,10 +320,10 @@ describe('JobArtifactsTable component', () => {
browseArtifactsPath: null,
};
- createComponent(
- { getJobArtifactsQuery: jest.fn() },
- { jobArtifacts: [jobWithoutBrowsePath] },
- );
+ createComponent({
+ handlers: { getJobArtifactsQuery: jest.fn() },
+ data: { jobArtifacts: [jobWithoutBrowsePath] },
+ });
await waitForPromises();
@@ -298,7 +333,7 @@ describe('JobArtifactsTable component', () => {
describe('delete button', () => {
it('does not show when user does not have permission', async () => {
- createComponent({}, {}, false);
+ createComponent({ canDestroyArtifacts: false });
await waitForPromises();
@@ -314,50 +349,125 @@ describe('JobArtifactsTable component', () => {
});
});
+ describe('bulk delete', () => {
+ describe('with permission and feature flag enabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('shows selected artifacts when a job is checked', async () => {
+ expect(findBulkDelete().exists()).toBe(false);
+
+ await findJobCheckbox().vm.$emit('input', true);
+
+ expect(findBulkDelete().exists()).toBe(true);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job.artifacts.nodes.map((node) => node.id),
+ );
+ });
+
+ it('disappears when selected artifacts are cleared', async () => {
+ await findJobCheckbox().vm.$emit('input', true);
+
+ expect(findBulkDelete().exists()).toBe(true);
+
+ await findBulkDelete().vm.$emit('clearSelectedArtifacts');
+
+ expect(findBulkDelete().exists()).toBe(false);
+ });
+
+ it('shows a toast when artifacts are deleted', async () => {
+ const count = job.artifacts.nodes.length;
+
+ await findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('deleted', count);
+
+ expect(mockToastShow).toHaveBeenCalledWith(`${count} selected artifacts deleted`);
+ });
+ });
+
+ it('shows no checkboxes without permission', async () => {
+ createComponent({
+ canDestroyArtifacts: false,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+
+ expect(findAnyCheckbox().exists()).toBe(false);
+ });
+
+ it('shows no checkboxes with feature flag disabled', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findAnyCheckbox().exists()).toBe(false);
+ });
+ });
+
describe('pagination', () => {
- const { pageInfo } = getJobArtifactsResponse.data.project.jobs;
+ const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs;
+ const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates);
beforeEach(async () => {
- createComponent(
- {
- getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates),
+ createComponent({
+ handlers: {
+ getJobArtifactsQuery: query,
},
- {
- count: enoughJobsToPaginate.length,
- pageInfo,
- },
- );
+ data: { pageInfo },
+ });
await waitForPromises();
});
it('renders pagination and passes page props', () => {
- expect(findPagination().exists()).toBe(true);
expect(findPagination().props()).toMatchObject({
- value: wrapper.vm.pagination.currentPage,
- prevPage: wrapper.vm.prevPage,
- nextPage: wrapper.vm.nextPage,
+ value: INITIAL_CURRENT_PAGE,
+ prevPage: Number(pageInfo.hasPreviousPage),
+ nextPage: Number(pageInfo.hasNextPage),
+ });
+
+ expect(query).toHaveBeenCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
});
});
- it('updates query variables when going to previous page', () => {
- return setPage(1).then(() => {
- expect(wrapper.vm.queryVariables).toMatchObject({
- projectPath: 'project/path',
- nextPageCursor: undefined,
- prevPageCursor: pageInfo.startCursor,
- });
+ it('updates query variables when going to previous page', async () => {
+ await setPage(1);
+
+ expect(query).toHaveBeenLastCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: null,
+ lastPageSize: JOBS_PER_PAGE,
+ prevPageCursor: pageInfo.startCursor,
});
+ expect(findPagination().props('value')).toEqual(1);
});
- it('updates query variables when going to next page', () => {
- return setPage(2).then(() => {
- expect(wrapper.vm.queryVariables).toMatchObject({
- lastPageSize: null,
- nextPageCursor: pageInfo.endCursor,
- prevPageCursor: '',
- });
+ it('updates query variables when going to next page', async () => {
+ await setPage(2);
+
+ expect(query).toHaveBeenLastCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: null,
+ prevPageCursor: '',
+ nextPageCursor: pageInfo.endCursor,
});
+ expect(findPagination().props('value')).toEqual(2);
});
});
});
diff --git a/spec/frontend/artifacts/components/job_checkbox_spec.js b/spec/frontend/artifacts/components/job_checkbox_spec.js
new file mode 100644
index 00000000000..95cc548b8c8
--- /dev/null
+++ b/spec/frontend/artifacts/components/job_checkbox_spec.js
@@ -0,0 +1,71 @@
+import { GlFormCheckbox } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import JobCheckbox from '~/artifacts/components/job_checkbox.vue';
+
+describe('JobCheckbox component', () => {
+ let wrapper;
+
+ const mockArtifactNodes = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes;
+ const mockSelectedArtifacts = [mockArtifactNodes[0], mockArtifactNodes[1]];
+ const mockUnselectedArtifacts = [mockArtifactNodes[2]];
+
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+
+ const createComponent = ({
+ hasArtifacts = true,
+ selectedArtifacts = mockSelectedArtifacts,
+ unselectedArtifacts = mockUnselectedArtifacts,
+ } = {}) => {
+ wrapper = shallowMountExtended(JobCheckbox, {
+ propsData: {
+ hasArtifacts,
+ selectedArtifacts,
+ unselectedArtifacts,
+ },
+ mocks: { GlFormCheckbox },
+ });
+ };
+
+ it('is disabled when the job has no artifacts', () => {
+ createComponent({ hasArtifacts: false });
+
+ expect(findCheckbox().attributes('disabled')).toBe('true');
+ });
+
+ describe('when some artifacts are selected', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is indeterminate', () => {
+ expect(findCheckbox().attributes('indeterminate')).toBe('true');
+ expect(findCheckbox().attributes('checked')).toBeUndefined();
+ });
+
+ it('selects the unselected artifacts on click', () => {
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([[mockUnselectedArtifacts[0], true]]);
+ });
+ });
+
+ describe('when all artifacts are selected', () => {
+ beforeEach(() => {
+ createComponent({ unselectedArtifacts: [] });
+ });
+
+ it('is checked', () => {
+ expect(findCheckbox().attributes('checked')).toBe('true');
+ });
+
+ it('deselects the selected artifacts on click', () => {
+ findCheckbox().vm.$emit('input', false);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockSelectedArtifacts[0], false],
+ [mockSelectedArtifacts[1], false],
+ ]);
+ });
+ });
+});
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 ca94acfa444..efdebe5f3b0 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
@@ -78,8 +78,6 @@ describe('Keep latest artifact checkbox', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
apolloProvider = null;
});
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
deleted file mode 100644
index 3ae7fcf1c49..00000000000
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import U2FAuthenticate from '~/authentication/u2f/authenticate';
-import 'vendor/u2f';
-import MockU2FDevice from './mock_u2f_device';
-
-describe('U2FAuthenticate', () => {
- let u2fDevice;
- let container;
- let component;
-
- beforeEach(() => {
- loadHTMLFixture('u2f/authenticate.html');
- u2fDevice = new MockU2FDevice();
- container = $('#js-authenticate-token-2fa');
- component = new U2FAuthenticate(
- container,
- '#js-login-token-2fa-form',
- {
- sign_requests: [],
- },
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form'),
- );
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('with u2f unavailable', () => {
- let oldu2f;
-
- beforeEach(() => {
- jest.spyOn(component, 'switchToFallbackUI').mockImplementation(() => {});
- oldu2f = window.u2f;
- window.u2f = null;
- });
-
- afterEach(() => {
- window.u2f = oldu2f;
- });
-
- it('falls back to normal 2fa', async () => {
- await component.start();
- expect(component.switchToFallbackUI).toHaveBeenCalled();
- });
- });
-
- describe('with u2f available', () => {
- beforeEach(() => {
- // bypass automatic form submission within renderAuthenticated
- jest.spyOn(component, 'renderAuthenticated').mockReturnValue(true);
- u2fDevice = new MockU2FDevice();
-
- return component.start();
- });
-
- it('allows authenticating via a U2F device', () => {
- const inProgressMessage = container.find('p');
-
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
- });
-
- expect(component.renderAuthenticated).toHaveBeenCalledWith(
- '{"deviceData":"this is data from the device"}',
- );
- });
-
- describe('errors', () => {
- it('displays an error message', () => {
- const setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('There was a problem communicating with your device');
- });
-
- it('allows retrying authentication after an error', () => {
- let setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const retryButton = container.find('#js-token-2fa-try-again');
- retryButton.trigger('click');
- setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
- });
-
- expect(component.renderAuthenticated).toHaveBeenCalledWith(
- '{"deviceData":"this is data from the device"}',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/authentication/u2f/mock_u2f_device.js b/spec/frontend/authentication/u2f/mock_u2f_device.js
deleted file mode 100644
index ec8425a4e3e..00000000000
--- a/spec/frontend/authentication/u2f/mock_u2f_device.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-unused-expressions */
-
-export default class MockU2FDevice {
- constructor() {
- this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
- this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
- window.u2f || (window.u2f = {});
- window.u2f.register = (appId, registerRequests, signRequests, callback) => {
- this.registerCallback = callback;
- };
- window.u2f.sign = (appId, challenges, signRequests, callback) => {
- this.authenticateCallback = callback;
- };
- }
-
- respondToRegisterRequest(params) {
- return this.registerCallback(params);
- }
-
- respondToAuthenticateRequest(params) {
- return this.authenticateCallback(params);
- }
-}
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
deleted file mode 100644
index 23d1e5c7dee..00000000000
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { trimText } from 'helpers/text_helper';
-import U2FRegister from '~/authentication/u2f/register';
-import 'vendor/u2f';
-import MockU2FDevice from './mock_u2f_device';
-
-describe('U2FRegister', () => {
- let u2fDevice;
- let container;
- let component;
-
- beforeEach(() => {
- loadHTMLFixture('u2f/register.html');
- u2fDevice = new MockU2FDevice();
- container = $('#js-register-token-2fa');
- component = new U2FRegister(container, {});
- return component.start();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('allows registering a U2F device', () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
-
- expect(trimText(setupButton.text())).toBe('Set up new device');
- setupButton.trigger('click');
- const inProgressMessage = container.children('p');
-
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- u2fDevice.respondToRegisterRequest({
- deviceData: 'this is data from the device',
- });
- const registeredMessage = container.find('p');
- const deviceResponse = container.find('#js-device-response');
-
- expect(registeredMessage.text()).toContain('Your device was successfully set up!');
- expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
- });
-
- describe('errors', () => {
- it("doesn't allow the same device to be registered twice (for the same user", () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 4,
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('already been registered with us');
- });
-
- it('displays an error message for other errors', () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 'error!',
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('There was a problem communicating with your device');
- });
-
- it('allows retrying registration after an error', () => {
- let setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 'error!',
- });
- const retryButton = container.find('#js-token-2fa-try-again');
- retryButton.trigger('click');
- setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- deviceData: 'this is data from the device',
- });
- const registeredMessage = container.find('p');
-
- expect(registeredMessage.text()).toContain('Your device was successfully set up!');
- });
- });
-});
diff --git a/spec/frontend/authentication/u2f/util_spec.js b/spec/frontend/authentication/u2f/util_spec.js
deleted file mode 100644
index 67fd4c73243..00000000000
--- a/spec/frontend/authentication/u2f/util_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { canInjectU2fApi } from '~/authentication/u2f/util';
-
-describe('U2F Utils', () => {
- describe('canInjectU2fApi', () => {
- it('returns false for Chrome < 41', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns true for Chrome >= 41', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(true);
- });
-
- it('returns false for Opera < 40', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns true for Opera >= 40', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991';
-
- expect(canInjectU2fApi(userAgent)).toBe(true);
- });
-
- it('returns false for Safari', () => {
- const userAgent =
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Chrome on Android', () => {
- const userAgent =
- 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Chrome on iOS', () => {
- const userAgent =
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Safari on iOS', () => {
- const userAgent =
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/authentication/webauthn/components/registration_spec.js b/spec/frontend/authentication/webauthn/components/registration_spec.js
new file mode 100644
index 00000000000..1221626db7d
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/components/registration_spec.js
@@ -0,0 +1,255 @@
+import { nextTick } from 'vue';
+import { GlAlert, GlButton, GlForm, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Registration from '~/authentication/webauthn/components/registration.vue';
+import {
+ I18N_BUTTON_REGISTER,
+ I18N_BUTTON_SETUP,
+ I18N_BUTTON_TRY_AGAIN,
+ I18N_ERROR_HTTP,
+ I18N_ERROR_UNSUPPORTED_BROWSER,
+ I18N_INFO_TEXT,
+ I18N_STATUS_SUCCESS,
+ I18N_STATUS_WAITING,
+ STATE_ERROR,
+ STATE_READY,
+ STATE_SUCCESS,
+ STATE_UNSUPPORTED,
+ STATE_WAITING,
+ WEBAUTHN_REGISTER,
+} from '~/authentication/webauthn/constants';
+import * as WebAuthnUtils from '~/authentication/webauthn/util';
+import WebAuthnError from '~/authentication/webauthn/error';
+
+const csrfToken = 'mock-csrf-token';
+jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken }));
+jest.mock('~/authentication/webauthn/util');
+jest.mock('~/authentication/webauthn/error');
+
+describe('Registration', () => {
+ const initialError = null;
+ const passwordRequired = true;
+ const targetPath = '/-/profile/two_factor_auth/create_webauthn';
+ let wrapper;
+
+ const createComponent = (provide = {}) => {
+ wrapper = shallowMountExtended(Registration, {
+ provide: { initialError, passwordRequired, targetPath, ...provide },
+ });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ describe(`when ${STATE_UNSUPPORTED} state`, () => {
+ it('shows an error if using unsecure scheme (HTTP)', () => {
+ // `supported` function returns false for HTTP because `navigator.credentials` is undefined.
+ WebAuthnUtils.supported.mockReturnValue(false);
+ WebAuthnUtils.isHTTPS.mockReturnValue(false);
+ createComponent();
+
+ const alert = wrapper.findComponent(GlAlert);
+ expect(alert.props('variant')).toBe('danger');
+ expect(alert.text()).toBe(I18N_ERROR_HTTP);
+ });
+
+ it('shows an error if using unsupported browser', () => {
+ WebAuthnUtils.supported.mockReturnValue(false);
+ WebAuthnUtils.isHTTPS.mockReturnValue(true);
+ createComponent();
+
+ const alert = wrapper.findComponent(GlAlert);
+ expect(alert.props('variant')).toBe('danger');
+ expect(alert.text()).toBe(I18N_ERROR_UNSUPPORTED_BROWSER);
+ });
+ });
+
+ describe('when scheme or browser are supported', () => {
+ const mockCreate = jest.fn();
+
+ const clickSetupDeviceButton = () => {
+ findButton().vm.$emit('click');
+ return nextTick();
+ };
+
+ const setupDevice = () => {
+ clickSetupDeviceButton();
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ WebAuthnUtils.isHTTPS.mockReturnValue(true);
+ WebAuthnUtils.supported.mockReturnValue(true);
+ global.navigator.credentials = { create: mockCreate };
+ gon.webauthn = { options: {} };
+ });
+
+ afterEach(() => {
+ global.navigator.credentials = undefined;
+ });
+
+ describe(`when ${STATE_READY} state`, () => {
+ it('shows button and explanation text', () => {
+ createComponent();
+
+ expect(findButton().text()).toBe(I18N_BUTTON_SETUP);
+ expect(wrapper.text()).toContain(I18N_INFO_TEXT);
+ });
+ });
+
+ describe(`when ${STATE_WAITING} state`, () => {
+ it('shows loading icon and message after pressing the button', async () => {
+ createComponent();
+
+ await clickSetupDeviceButton();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.text()).toContain(I18N_STATUS_WAITING);
+ });
+ });
+
+ describe(`when ${STATE_SUCCESS} state`, () => {
+ const credentials = 1;
+
+ const findCurrentPasswordInput = () => wrapper.findByTestId('current-password-input');
+ const findDeviceNameInput = () => wrapper.findByTestId('device-name-input');
+
+ beforeEach(() => {
+ mockCreate.mockResolvedValueOnce(true);
+ WebAuthnUtils.convertCreateResponse.mockReturnValue(credentials);
+ });
+
+ describe('registration form', () => {
+ it('has correct action', async () => {
+ createComponent();
+
+ await setupDevice();
+
+ expect(wrapper.findComponent(GlForm).attributes('action')).toBe(targetPath);
+ });
+
+ describe('when password is required', () => {
+ it('shows device name and password fields', async () => {
+ createComponent();
+
+ await setupDevice();
+
+ expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS);
+
+ // Visible inputs
+ expect(findCurrentPasswordInput().attributes('name')).toBe('current_password');
+ expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]');
+
+ // Hidden inputs
+ expect(
+ wrapper
+ .find('input[name="device_registration[device_response]"]')
+ .attributes('value'),
+ ).toBe(`${credentials}`);
+ expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe(
+ csrfToken,
+ );
+
+ expect(findButton().text()).toBe(I18N_BUTTON_REGISTER);
+ });
+
+ it('enables the register device button when device name and password are filled', async () => {
+ createComponent();
+
+ await setupDevice();
+
+ expect(findButton().props('disabled')).toBe(true);
+
+ // Visible inputs
+ findCurrentPasswordInput().vm.$emit('input', 'my current password');
+ findDeviceNameInput().vm.$emit('input', 'my device name');
+ await nextTick();
+
+ expect(findButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('when password is not required', () => {
+ it('shows a device name field', async () => {
+ createComponent({ passwordRequired: false });
+
+ await setupDevice();
+
+ expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS);
+
+ // Visible inputs
+ expect(findCurrentPasswordInput().exists()).toBe(false);
+ expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]');
+
+ // Hidden inputs
+ expect(
+ wrapper
+ .find('input[name="device_registration[device_response]"]')
+ .attributes('value'),
+ ).toBe(`${credentials}`);
+ expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe(
+ csrfToken,
+ );
+
+ expect(findButton().text()).toBe(I18N_BUTTON_REGISTER);
+ });
+
+ it('enables the register device button when device name is filled', async () => {
+ createComponent({ passwordRequired: false });
+
+ await setupDevice();
+
+ expect(findButton().props('disabled')).toBe(true);
+
+ findDeviceNameInput().vm.$emit('input', 'my device name');
+ await nextTick();
+
+ expect(findButton().props('disabled')).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe(`when ${STATE_ERROR} state`, () => {
+ it('shows an initial error message and a retry button', async () => {
+ const myError = 'my error';
+ createComponent({ initialError: myError });
+
+ const alert = wrapper.findComponent(GlAlert);
+ expect(alert.props()).toMatchObject({
+ variant: 'danger',
+ secondaryButtonText: I18N_BUTTON_TRY_AGAIN,
+ });
+ expect(alert.text()).toContain(myError);
+ });
+
+ it('shows an error message and a retry button', async () => {
+ createComponent();
+ const error = new Error();
+ mockCreate.mockRejectedValueOnce(error);
+
+ await setupDevice();
+
+ expect(WebAuthnError).toHaveBeenCalledWith(error, WEBAUTHN_REGISTER);
+ expect(wrapper.findComponent(GlAlert).props()).toMatchObject({
+ variant: 'danger',
+ secondaryButtonText: I18N_BUTTON_TRY_AGAIN,
+ });
+ });
+
+ it('recovers after an error (error to success state)', async () => {
+ createComponent();
+ mockCreate.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(true);
+
+ await setupDevice();
+
+ expect(wrapper.findComponent(GlAlert).props('variant')).toBe('danger');
+
+ wrapper.findComponent(GlAlert).vm.$emit('secondaryAction');
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlAlert).props('variant')).toBe('info');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/webauthn/error_spec.js b/spec/frontend/authentication/webauthn/error_spec.js
index 9b71f77dde2..b979173edc6 100644
--- a/spec/frontend/authentication/webauthn/error_spec.js
+++ b/spec/frontend/authentication/webauthn/error_spec.js
@@ -1,16 +1,17 @@
import setWindowLocation from 'helpers/set_window_location_helper';
import WebAuthnError from '~/authentication/webauthn/error';
+import { WEBAUTHN_AUTHENTICATE, WEBAUTHN_REGISTER } from '~/authentication/webauthn/constants';
describe('WebAuthnError', () => {
it.each([
[
'NotSupportedError',
'Your device is not compatible with GitLab. Please try another device',
- 'authenticate',
+ WEBAUTHN_AUTHENTICATE,
],
- ['InvalidStateError', 'This device has not been registered with us.', 'authenticate'],
- ['InvalidStateError', 'This device has already been registered with us.', 'register'],
- ['UnknownError', 'There was a problem communicating with your device.', 'register'],
+ ['InvalidStateError', 'This device has not been registered with us.', WEBAUTHN_AUTHENTICATE],
+ ['InvalidStateError', 'This device has already been registered with us.', WEBAUTHN_REGISTER],
+ ['UnknownError', 'There was a problem communicating with your device.', WEBAUTHN_REGISTER],
])('exception %s will have message %s, flow type: %s', (exception, expectedMessage, flowType) => {
expect(new WebAuthnError(new DOMException('', exception), flowType).message()).toEqual(
expectedMessage,
@@ -24,7 +25,7 @@ describe('WebAuthnError', () => {
const expectedMessage =
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.';
expect(
- new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
+ new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(),
).toEqual(expectedMessage);
});
@@ -33,7 +34,7 @@ describe('WebAuthnError', () => {
const expectedMessage = 'There was a problem communicating with your device.';
expect(
- new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
+ new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(),
).toEqual(expectedMessage);
});
});
diff --git a/spec/frontend/authentication/webauthn/util_spec.js b/spec/frontend/authentication/webauthn/util_spec.js
index bc44b47d0ba..831d1636b8c 100644
--- a/spec/frontend/authentication/webauthn/util_spec.js
+++ b/spec/frontend/authentication/webauthn/util_spec.js
@@ -1,4 +1,9 @@
-import { base64ToBuffer, bufferToBase64, base64ToBase64Url } from '~/authentication/webauthn/util';
+import {
+ base64ToBuffer,
+ bufferToBase64,
+ base64ToBase64Url,
+ supported,
+} from '~/authentication/webauthn/util';
const encodedString = 'SGVsbG8gd29ybGQh';
const stringBytes = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33];
@@ -31,4 +36,28 @@ describe('Webauthn utils', () => {
expect(base64ToBase64Url(argument)).toBe(expectedResult);
});
});
+
+ describe('supported', () => {
+ afterEach(() => {
+ global.navigator.credentials = undefined;
+ window.PublicKeyCredential = undefined;
+ });
+
+ it.each`
+ credentials | PublicKeyCredential | expected
+ ${undefined} | ${undefined} | ${false}
+ ${{}} | ${undefined} | ${false}
+ ${{ create: true }} | ${undefined} | ${false}
+ ${{ create: true, get: true }} | ${undefined} | ${false}
+ ${{ create: true, get: true }} | ${true} | ${true}
+ `(
+ 'returns $expected when credentials is $credentials and PublicKeyCredential is $PublicKeyCredential',
+ ({ credentials, PublicKeyCredential, expected }) => {
+ global.navigator.credentials = credentials;
+ window.PublicKeyCredential = PublicKeyCredential;
+
+ expect(supported()).toBe(expected);
+ },
+ );
+ });
});
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index 1a54b9909ba..35a1603d375 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -6,10 +6,8 @@ import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_fra
import loadAwardsHandler from '~/awards_handler';
window.gl = window.gl || {};
-window.gon = window.gon || {};
let awardsHandler = null;
-const urlRoot = gon.relative_url_root;
describe('AwardsHandler', () => {
useFakeRequestAnimationFrame();
@@ -95,9 +93,6 @@ describe('AwardsHandler', () => {
});
afterEach(() => {
- // restore original url root value
- gon.relative_url_root = urlRoot;
-
clearEmojiMock();
// Undo what we did to the shared <body>
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index 0a736df7075..d7519f1f80d 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -43,7 +43,6 @@ describe('BadgeForm component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index ee7ccac974a..cbbeb36ff33 100644
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -43,7 +43,6 @@ describe('BadgeListRow component', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 606b1bc9cce..374b7b50af4 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -38,10 +38,6 @@ describe('BadgeList component', () => {
wrapper = mount(BadgeList, { store });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('for project badges', () => {
it('renders a header with the badge count', () => {
createComponent({
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index bddb6d3801c..7ad2c99869c 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -32,10 +32,6 @@ describe('BadgeSettings component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays modal if button for deleting a badge is clicked', async () => {
const button = wrapper.find('[data-testid="delete-badge"]');
diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index b468e38f19e..c933c1b5434 100644
--- a/spec/frontend/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -24,10 +24,6 @@ describe('Badge component', () => {
wrapper = mount(Badge, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
return createComponent({ ...dummyProps }, '#dummy-element');
});
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 c922d6a9809..f667ebc0fcb 100644
--- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
+++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
@@ -28,10 +28,6 @@ describe('Batch comments diff file drafts component', () => {
});
}
- afterEach(() => {
- vm.destroy();
- });
-
it('renders list of draft notes', () => {
factory();
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 924d88866ee..159e36c1364 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -49,10 +49,6 @@ describe('Batch comments draft note component', () => {
draft = createDraft();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders template', () => {
createComponent();
expect(wrapper.findComponent(GlBadge).exists()).toBe(true);
diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js
index c3a7946c85c..850a7efb4ed 100644
--- a/spec/frontend/batch_comments/components/drafts_count_spec.js
+++ b/spec/frontend/batch_comments/components/drafts_count_spec.js
@@ -15,10 +15,6 @@ describe('Batch comments drafts count component', () => {
wrapper = mount(DraftsCount, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders count', () => {
expect(wrapper.text()).toContain('1');
});
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index f86e003ab5f..3a28bf4ade8 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -1,7 +1,6 @@
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlDisclosureDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
@@ -46,9 +45,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
},
});
- wrapper = shallowMount(PreviewDropdown, {
+ wrapper = mount(PreviewDropdown, {
store,
- stubs: { GlDisclosureDropdown },
+ stubs: {
+ PreviewItem: true,
+ },
});
}
@@ -59,12 +60,12 @@ describe('Batch comments preview dropdown', () => {
viewDiffsFileByFile: true,
sortedDrafts: [{ id: 1, file_hash: 'hash' }],
});
-
- findPreviewItem().vm.$emit('click');
-
+ findPreviewItem().trigger('click');
await nextTick();
expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
+
+ await nextTick();
expect(scrollToDraft).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ id: 1, file_hash: 'hash' }),
@@ -77,7 +78,7 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1 }],
});
- findPreviewItem().vm.$emit('click');
+ findPreviewItem().trigger('click');
await nextTick();
@@ -93,7 +94,7 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }],
});
- findPreviewItem().vm.$emit('click');
+ findPreviewItem().trigger('click');
await nextTick();
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 6a99294f855..a19a72af813 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -26,10 +26,6 @@ describe('Batch comments draft preview item component', () => {
wrapper = mount(PreviewItem, { store, propsData: { draft, isLast } });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders text content', () => {
createComponent(false, { note_html: '<img src="" /><p>Hello world</p>' });
diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js
index 0a4c9ff62e4..923e86a7e64 100644
--- a/spec/frontend/batch_comments/components/review_bar_spec.js
+++ b/spec/frontend/batch_comments/components/review_bar_spec.js
@@ -20,10 +20,6 @@ describe('Batch comments review bar component', () => {
document.body.className = '';
});
- afterEach(() => {
- wrapper.destroy();
- });
-
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);
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 003a6d86371..ac6198ec8b5 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -47,7 +47,6 @@ const findForm = () => wrapper.findByTestId('submit-gl-form');
describe('Batch comments submit dropdown', () => {
afterEach(() => {
- wrapper.destroy();
window.mrTabs = null;
});
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 20eedcbb25b..57bafb51cd6 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
@@ -317,4 +317,10 @@ describe('Batch comments store actions', () => {
expect(window.mrTabs.tabShown).toHaveBeenCalledWith('diffs');
});
});
+
+ describe('clearDrafts', () => {
+ it('commits CLEAR_DRAFTS', () => {
+ return testAction(actions.clearDrafts, null, null, [{ type: 'CLEAR_DRAFTS' }], []);
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
index fe01de638c2..ad6a1a38164 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
@@ -104,4 +104,14 @@ describe('Batch comments mutations', () => {
]);
});
});
+
+ describe(types.CLEAR_DRAFTS, () => {
+ it('clears drafts array', () => {
+ state.drafts.push({ id: 1 });
+
+ mutations[types.CLEAR_DRAFTS](state);
+
+ expect(state.drafts).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/behaviors/components/diagram_performance_warning_spec.js b/spec/frontend/behaviors/components/diagram_performance_warning_spec.js
index c58c2bc55a9..7e6b20da4d4 100644
--- a/spec/frontend/behaviors/components/diagram_performance_warning_spec.js
+++ b/spec/frontend/behaviors/components/diagram_performance_warning_spec.js
@@ -11,10 +11,6 @@ describe('DiagramPerformanceWarning component', () => {
wrapper = shallowMount(DiagramPerformanceWarning);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders warning alert with button', () => {
expect(findAlert().props()).toMatchObject({
primaryButtonText: DiagramPerformanceWarning.i18n.buttonText,
diff --git a/spec/frontend/behaviors/components/json_table_spec.js b/spec/frontend/behaviors/components/json_table_spec.js
index 42b4a051d4d..a82310873ed 100644
--- a/spec/frontend/behaviors/components/json_table_spec.js
+++ b/spec/frontend/behaviors/components/json_table_spec.js
@@ -59,10 +59,6 @@ describe('behaviors/components/json_table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTable = () => wrapper.findComponent(GlTable);
const findTableCaption = () => wrapper.findByTestId('slot-table-caption');
const findFilterInput = () => wrapper.findComponent(GlFormInput);
diff --git a/spec/frontend/behaviors/copy_to_clipboard_spec.js b/spec/frontend/behaviors/copy_to_clipboard_spec.js
index c5beaa0ba5d..74a396eb8cb 100644
--- a/spec/frontend/behaviors/copy_to_clipboard_spec.js
+++ b/spec/frontend/behaviors/copy_to_clipboard_spec.js
@@ -31,7 +31,7 @@ describe('initCopyToClipboard', () => {
const defaultButtonAttributes = {
'data-clipboard-text': 'foo bar',
title,
- 'data-title': title,
+ 'data-original-title': title,
};
const createButton = (attributes = {}) => {
const combinedAttributes = { ...defaultButtonAttributes, ...attributes };
diff --git a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
index 38d19ac3808..ad70efdf7c3 100644
--- a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
+++ b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
@@ -22,14 +22,9 @@ describe('highlightCurrentUser', () => {
describe('without current user', () => {
beforeEach(() => {
- window.gon = window.gon || {};
window.gon.current_user_id = null;
});
- afterEach(() => {
- delete window.gon.current_user_id;
- });
-
it('does not highlight the user', () => {
const initialHtml = rootElement.outerHTML;
@@ -41,14 +36,9 @@ describe('highlightCurrentUser', () => {
describe('with current user', () => {
beforeEach(() => {
- window.gon = window.gon || {};
window.gon.current_user_id = 2;
});
- afterEach(() => {
- delete window.gon.current_user_id;
- });
-
it('highlights current user', () => {
highlightCurrentUser(elements);
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
index c87d11742dc..f464c01ac15 100644
--- a/spec/frontend/behaviors/markdown/render_observability_spec.js
+++ b/spec/frontend/behaviors/markdown/render_observability_spec.js
@@ -1,38 +1,43 @@
+import Vue from 'vue';
+import { createWrapper } from '@vue/test-utils';
import renderObservability from '~/behaviors/markdown/render_observability';
-import * as ColorUtils from '~/lib/utils/color_utils';
+import { INLINE_EMBED_DIMENSIONS, SKELETON_VARIANT_EMBED } from '~/observability/constants';
+import ObservabilityApp from '~/observability/components/observability_app.vue';
-describe('Observability iframe renderer', () => {
- const findObservabilityIframes = (theme = 'light') =>
- document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`);
-
- const renderEmbeddedObservability = () => {
- renderObservability([...document.querySelectorAll('.js-render-observability')]);
- jest.runAllTimers();
- };
+describe('renderObservability', () => {
+ let subject;
beforeEach(() => {
- document.body.dataset.page = '';
- document.body.innerHTML = '';
+ subject = document.createElement('div');
+ subject.classList.add('js-render-observability');
+ subject.dataset.frameUrl = 'https://observe.gitlab.com/';
+ document.body.appendChild(subject);
});
- it('renders an observability iframe', () => {
- document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
-
- expect(findObservabilityIframes()).toHaveLength(0);
-
- renderEmbeddedObservability();
-
- expect(findObservabilityIframes()).toHaveLength(1);
+ afterEach(() => {
+ subject.remove();
});
- it('renders iframe with dark param when GL has dark theme', () => {
- document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/"></div>`;
- jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
+ it('should return an array of Vue instances', () => {
+ const vueInstances = renderObservability([
+ ...document.querySelectorAll('.js-render-observability'),
+ ]);
+ expect(vueInstances).toEqual([expect.any(Vue)]);
+ });
- expect(findObservabilityIframes('dark')).toHaveLength(0);
+ it('should correctly pass props to the ObservabilityApp component', () => {
+ const vueInstances = renderObservability([
+ ...document.querySelectorAll('.js-render-observability'),
+ ]);
- renderEmbeddedObservability();
+ const wrapper = createWrapper(vueInstances[0]);
- expect(findObservabilityIframes('dark')).toHaveLength(1);
+ expect(wrapper.findComponent(ObservabilityApp).props()).toMatchObject({
+ observabilityIframeSrc: 'https://observe.gitlab.com/',
+ skeletonVariant: SKELETON_VARIANT_EMBED,
+ inlineEmbed: true,
+ height: INLINE_EMBED_DIMENSIONS.HEIGHT,
+ width: INLINE_EMBED_DIMENSIONS.WIDTH,
+ });
});
});
diff --git a/spec/frontend/blame/blame_redirect_spec.js b/spec/frontend/blame/blame_redirect_spec.js
index beb10139b3a..326f60a5b13 100644
--- a/spec/frontend/blame/blame_redirect_spec.js
+++ b/spec/frontend/blame/blame_redirect_spec.js
@@ -1,8 +1,8 @@
import redirectToCorrectPage from '~/blame/blame_redirect';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Blame page redirect', () => {
beforeEach(() => {
diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js
new file mode 100644
index 00000000000..e048ce3f70e
--- /dev/null
+++ b/spec/frontend/blame/streaming/index_spec.js
@@ -0,0 +1,110 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { renderBlamePageStreams } from '~/blame/streaming';
+import { setHTMLFixture } from 'helpers/fixtures';
+import { renderHtmlStreams } from '~/streaming/render_html_streams';
+import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
+import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { toPolyfillReadable } from '~/streaming/polyfills';
+import { createAlert } from '~/alert';
+
+jest.mock('~/streaming/render_html_streams');
+jest.mock('~/streaming/rate_limit_stream_requests');
+jest.mock('~/streaming/handle_streamed_anchor_link');
+jest.mock('~/streaming/polyfills');
+jest.mock('~/sentry');
+jest.mock('~/alert');
+
+global.fetch = jest.fn();
+
+describe('renderBlamePageStreams', () => {
+ let stopAnchor;
+ const PAGES_URL = 'https://example.com/';
+ const findStreamContainer = () => document.querySelector('#blame-stream-container');
+ const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading');
+
+ const setupHtml = (totalExtraPages = 0) => {
+ setHTMLFixture(`
+ <div id="blob-content-holder"
+ data-total-extra-pages="${totalExtraPages}"
+ data-pages-url="${PAGES_URL}"
+ ></div>
+ <div id="blame-stream-container"></div>
+ <div id="blame-stream-loading"></div>
+ `);
+ };
+
+ handleStreamedAnchorLink.mockImplementation(() => stopAnchor);
+ rateLimitStreamRequests.mockImplementation(({ factory, total }) => {
+ return Array.from({ length: total }, (_, i) => {
+ return Promise.resolve(factory(i));
+ });
+ });
+ toPolyfillReadable.mockImplementation((obj) => obj);
+
+ beforeEach(() => {
+ stopAnchor = jest.fn();
+ fetch.mockClear();
+ });
+
+ it('does nothing for an empty page', async () => {
+ await renderBlamePageStreams();
+
+ expect(handleStreamedAnchorLink).not.toHaveBeenCalled();
+ expect(renderHtmlStreams).not.toHaveBeenCalled();
+ });
+
+ it('renders a single stream', async () => {
+ let res;
+ const stream = new Promise((resolve) => {
+ res = resolve;
+ });
+ renderHtmlStreams.mockImplementationOnce(() => stream);
+ setupHtml();
+
+ renderBlamePageStreams(stream);
+
+ expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1);
+ expect(stopAnchor).toHaveBeenCalledTimes(0);
+ expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer());
+ expect(findStreamLoadingIndicator()).not.toBe(null);
+
+ res();
+ await waitForPromises();
+
+ expect(stopAnchor).toHaveBeenCalledTimes(1);
+ expect(findStreamLoadingIndicator()).toBe(null);
+ });
+
+ it('renders rest of the streams', async () => {
+ const stream = Promise.resolve();
+ const stream2 = Promise.resolve({ body: null });
+ fetch.mockImplementationOnce(() => stream2);
+ setupHtml(1);
+
+ await renderBlamePageStreams(stream);
+
+ expect(fetch.mock.calls[0][0].toString()).toBe(`${PAGES_URL}?page=3`);
+ expect(renderHtmlStreams).toHaveBeenCalledWith([stream, stream2], findStreamContainer());
+ });
+
+ it('shows an error message when failed', async () => {
+ const stream = Promise.resolve();
+ const error = new Error();
+ renderHtmlStreams.mockImplementationOnce(() => Promise.reject(error));
+ setupHtml();
+
+ try {
+ await renderBlamePageStreams(stream);
+ } catch (err) {
+ expect(err).toBe(error);
+ }
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Blame could not be loaded as a single page.',
+ primaryButton: {
+ text: 'View blame as separate pages',
+ clickHandler: expect.any(Function),
+ },
+ });
+ });
+});
diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js
index 0f5885c2acf..203fab94a5c 100644
--- a/spec/frontend/blob/components/blob_content_error_spec.js
+++ b/spec/frontend/blob/components/blob_content_error_spec.js
@@ -18,10 +18,6 @@ describe('Blob Content Error component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('collapsed and too large blobs', () => {
it.each`
error | reason | options
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index f7b819b6e94..91af5f7bfed 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -29,10 +29,6 @@ describe('Blob Content component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('renders loader if `loading: true`', () => {
createComponent({ loading: true });
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index c84b5896348..2b1bd1ac4ad 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -22,10 +22,6 @@ describe('Blob Header Editing', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 0f015715dc2..e12021a48d2 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -34,10 +34,6 @@ describe('Blob Header Default Actions', () => {
buttons = wrapper.findAllComponents(GlButton);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders', () => {
const findCopyButton = () => wrapper.findByTestId('copyContentsButton');
const findViewRawButton = () => wrapper.findByTestId('viewRawButton');
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 8c32cba1ba4..be49146ff8a 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -21,10 +21,6 @@ describe('Blob Header Filepath', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findBadge = () => wrapper.findComponent(GlBadge);
describe('rendering', () => {
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 46740958090..47e09bb38bc 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -1,9 +1,14 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
+import {
+ RICH_BLOB_VIEWER_TITLE,
+ SIMPLE_BLOB_VIEWER,
+ SIMPLE_BLOB_VIEWER_TITLE,
+} from '~/blob/components/constants';
import TableContents from '~/blob/components/table_contents.vue';
import { Blob } from './mock_data';
@@ -11,12 +16,26 @@ import { Blob } from './mock_data';
describe('Blob Header Default Actions', () => {
let wrapper;
- function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) {
- const method = shouldMount ? mount : shallowMount;
- const blobHash = 'foo-bar';
- wrapper = method.call(this, BlobHeader, {
+ const defaultProvide = {
+ blobHash: 'foo-bar',
+ };
+
+ const findDefaultActions = () => wrapper.findComponent(DefaultActions);
+ const findTableContents = () => wrapper.findComponent(TableContents);
+ const findViewSwitcher = () => wrapper.findComponent(ViewerSwitcher);
+ const findBlobFilePath = () => wrapper.findComponent(BlobFilepath);
+ const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE);
+ const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE);
+
+ function createComponent({
+ blobProps = {},
+ options = {},
+ propsData = {},
+ mountFn = shallowMount,
+ } = {}) {
+ wrapper = mountFn(BlobHeader, {
provide: {
- blobHash,
+ ...defaultProvide,
},
propsData: {
blob: { ...Blob, ...blobProps },
@@ -26,143 +45,123 @@ describe('Blob Header Default Actions', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
- const findDefaultActions = () => wrapper.findComponent(DefaultActions);
-
- const slots = {
- prepend: 'Foo Prepend',
- actions: 'Actions Bar',
- };
-
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
- it('renders all components', () => {
- createComponent();
- expect(wrapper.findComponent(TableContents).exists()).toBe(true);
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true);
- expect(findDefaultActions().exists()).toBe(true);
- expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true);
+ describe('default render', () => {
+ it.each`
+ findComponent | componentName
+ ${findTableContents} | ${'TableContents'}
+ ${findViewSwitcher} | ${'ViewSwitcher'}
+ ${findDefaultActions} | ${'DefaultActions'}
+ ${findBlobFilePath} | ${'BlobFilePath'}
+ `('renders $componentName component by default', ({ findComponent }) => {
+ createComponent();
+
+ expect(findComponent().exists()).toBe(true);
+ });
});
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
- richViewer: null,
+ blobProps: {
+ richViewer: null,
+ },
});
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hideViewerSwitcher: true,
},
- );
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
+ });
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('does not render default actions is corresponding prop is passed', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hideDefaultActions: true,
},
- );
- expect(wrapper.findComponent(DefaultActions).exists()).toBe(false);
+ });
+ expect(findDefaultActions().exists()).toBe(false);
});
- Object.keys(slots).forEach((slot) => {
- it('renders the slots', () => {
- const slotContent = slots[slot];
- createComponent(
- {},
- {
- scopedSlots: {
- [slot]: `<span>${slotContent}</span>`,
- },
+ it.each`
+ slotContent | key
+ ${'Foo Prepend'} | ${'prepend'}
+ ${'Actions Bar'} | ${'actions'}
+ `('renders the slot $key', ({ key, slotContent }) => {
+ createComponent({
+ options: {
+ scopedSlots: {
+ [key]: `<span>${slotContent}</span>`,
},
- {},
- true,
- );
- expect(wrapper.text()).toContain(slotContent);
+ },
+ mountFn: mount,
});
+ expect(wrapper.text()).toContain(slotContent);
});
it('passes information about render error down to default actions', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hasRenderError: true,
},
- );
+ });
expect(findDefaultActions().props('hasRenderError')).toBe(true);
});
it('passes the correct isBinary value to default actions when viewing a binary file', () => {
- createComponent({}, {}, { isBinary: true });
+ createComponent({ propsData: { isBinary: true } });
expect(findDefaultActions().props('isBinary')).toBe(true);
});
});
describe('functionality', () => {
- const newViewer = 'Foo Bar';
- const activeViewerType = 'Alpha Beta';
-
const factory = (hideViewerSwitcher = false) => {
- createComponent(
- {},
- {},
- {
- activeViewerType,
+ createComponent({
+ propsData: {
+ activeViewerType: SIMPLE_BLOB_VIEWER,
hideViewerSwitcher,
},
- );
+ mountFn: mountExtended,
+ });
};
- it('by default sets viewer data based on activeViewerType', () => {
+ it('shows the correctly selected view by default', () => {
factory();
- expect(wrapper.vm.viewer).toBe(activeViewerType);
+
+ expect(findViewSwitcher().exists()).toBe(true);
+ expect(findRichTextEditorBtn().props().selected).toBe(false);
+ expect(findSimpleTextEditorBtn().props().selected).toBe(true);
});
- it('sets viewer to null if the viewer switcher should be hidden', () => {
+ it('Does not show the viewer switcher should be hidden', () => {
factory(true);
- expect(wrapper.vm.viewer).toBe(null);
+
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('watches the changes in viewer data and emits event when the change is registered', async () => {
factory();
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.viewer = newViewer;
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
- });
-
- it('does not emit event if the switcher is not rendered', async () => {
- factory(true);
-
- expect(wrapper.vm.showViewerSwitcher).toBe(false);
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.viewer = newViewer;
+ await findRichTextEditorBtn().trigger('click');
- await nextTick();
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('viewer-changed')).toBeDefined();
});
it('sets different icons depending on the blob file type', async () => {
factory();
- expect(wrapper.vm.blobSwitcherDocIcon).toBe('document');
+
+ expect(findViewSwitcher().props('docIcon')).toBe('document');
+
await wrapper.setProps({
blob: {
...Blob,
@@ -172,7 +171,8 @@ describe('Blob Header Default Actions', () => {
},
},
});
- expect(wrapper.vm.blobSwitcherDocIcon).toBe('table');
+
+ expect(findViewSwitcher().props('docIcon')).toBe('table');
});
});
});
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 1eac0733646..2ef87f6664b 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -18,14 +18,14 @@ describe('Blob Header Viewer Switcher', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
+ const findSimpleViewerButton = () => wrapper.findComponent('[data-viewer="simple"]');
+ const findRichViewerButton = () => wrapper.findComponent('[data-viewer="rich"]');
describe('intiialization', () => {
it('is initialized with simple viewer as active', () => {
createComponent();
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
+ expect(findRichViewerButton().props('selected')).toBe(false);
});
});
@@ -52,45 +52,34 @@ describe('Blob Header Viewer Switcher', () => {
});
describe('viewer changes', () => {
- let buttons;
- let simpleBtn;
- let richBtn;
+ it('does not switch the viewer if the selected one is already active', async () => {
+ createComponent();
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
- function factory(propsData = {}) {
- createComponent(propsData);
- buttons = wrapper.findAllComponents(GlButton);
- simpleBtn = buttons.at(0);
- richBtn = buttons.at(1);
-
- jest.spyOn(wrapper.vm, '$emit');
- }
-
- it('does not switch the viewer if the selected one is already active', () => {
- factory();
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
- simpleBtn.vm.$emit('click');
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ findSimpleViewerButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
+ expect(wrapper.emitted('input')).toBe(undefined);
});
it('emits an event when a Rich Viewer button is clicked', async () => {
- factory();
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
-
- richBtn.vm.$emit('click');
+ createComponent();
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
+ findRichViewerButton().vm.$emit('click');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER);
+
+ expect(wrapper.emitted('input')).toEqual([[RICH_BLOB_VIEWER]]);
});
it('emits an event when a Simple Viewer button is clicked', async () => {
- factory({
- value: RICH_BLOB_VIEWER,
- });
- simpleBtn.vm.$emit('click');
+ createComponent({ value: RICH_BLOB_VIEWER });
+ findSimpleViewerButton().vm.$emit('click');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER);
+
+ expect(wrapper.emitted('input')).toEqual([[SIMPLE_BLOB_VIEWER]]);
});
});
});
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 6af9cdcae7d..acfcef9704c 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -31,7 +31,6 @@ describe('Markdown table of contents component', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js
index 9364f76da5e..8f105f04aa7 100644
--- a/spec/frontend/blob/csv/csv_viewer_spec.js
+++ b/spec/frontend/blob/csv/csv_viewer_spec.js
@@ -29,10 +29,6 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(PapaParseAlert);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render loading spinner', () => {
createComponent();
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index 2e7eadc912d..97b32a42afe 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -42,8 +42,6 @@ describe('iPython notebook renderer', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mock.restore();
});
diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js
index 23227df6357..19d404f504b 100644
--- a/spec/frontend/blob/pdf/pdf_viewer_spec.js
+++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js
@@ -26,11 +26,6 @@ describe('PDF renderer', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('shows loading icon', () => {
expect(findLoading().exists()).toBe(true);
});
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 81b38cfc278..84efa6041e4 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -38,7 +38,6 @@ describe('PipelineTourSuccessModal', () => {
});
afterEach(() => {
- wrapper.destroy();
unmockTracking();
Cookies.remove(modalProps.commitCookie);
});
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index 6b329dc078a..b30b0287a34 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -36,11 +36,6 @@ describe('Suggest gitlab-ci.yml Popover', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when no dismiss cookie is set', () => {
beforeEach(() => {
createWrapper(defaultTrackLabel);
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index ed42322b0e6..89d507b4ec5 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -5,10 +5,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
import SourceEditor from '~/blob_edit/edit_blob';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
jest.mock('~/blob_edit/edit_blob');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('BlobBundle', () => {
it('does not load SourceEditor by default', () => {
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index dda46e97b85..9ab20fc2cd7 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -20,9 +20,9 @@ jest.mock('~/editor/extensions/source_editor_toolbar_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
+ { definition: ToolbarExtension },
{ definition: SourceEditorExtension },
{ definition: FileTemplateExtension },
- { definition: ToolbarExtension },
];
const markdownExtensions = [
{ definition: EditorMarkdownExtension },
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 1e823e3321a..a612e863d46 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -84,7 +84,7 @@ describe('Board card component', () => {
BoardCardMoveToPosition: true,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
provide: {
rootPath: '/',
@@ -110,8 +110,6 @@ describe('Board card component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
store = null;
jest.clearAllMocks();
});
@@ -314,10 +312,6 @@ describe('Board card component', () => {
});
});
- afterEach(() => {
- global.gon.default_avatar_url = null;
- });
-
it('displays defaults avatar if users avatar is null', () => {
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index d882ff071b7..43cf6ead1c1 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -92,6 +92,7 @@ export default function createComponent({
boardItems: [issue],
canAdminList: true,
boardId: 'gid://gitlab/Board/1',
+ filterParams: {},
...componentProps,
},
provide: {
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index fc8dbf8dc3a..9ec43c6e892 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -36,10 +36,6 @@ describe('Board list component', () => {
useFakeRequestAnimationFrame();
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When Expanded', () => {
beforeEach(() => {
wrapper = createComponent({ issuesCount: 1 });
diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js
index 0b3c6cb24c4..4fc9a6859a6 100644
--- a/spec/frontend/boards/components/board_add_new_column_form_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js
@@ -1,15 +1,13 @@
-import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
import { mockLabelList } from '../mock_data';
Vue.use(Vuex);
-describe('Board card layout', () => {
+describe('BoardAddNewColumnForm', () => {
let wrapper;
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
@@ -23,56 +21,30 @@ describe('Board card layout', () => {
});
};
- const mountComponent = ({
- loading = false,
- noneSelected = '',
- searchLabel = '',
- searchPlaceholder = '',
- selectedId,
- actions,
- slots,
- } = {}) => {
- wrapper = extendedWrapper(
- shallowMount(BoardAddNewColumnForm, {
- propsData: {
- loading,
- noneSelected,
- searchLabel,
- searchPlaceholder,
- selectedId,
- },
- slots,
- store: createStore({
- actions: {
- setAddColumnFormVisibility: jest.fn(),
- ...actions,
- },
- }),
- stubs: {
- GlDropdown,
+ const mountComponent = ({ searchLabel = '', selectedIdValid = true, actions, slots } = {}) => {
+ wrapper = shallowMountExtended(BoardAddNewColumnForm, {
+ propsData: {
+ searchLabel,
+ selectedIdValid,
+ },
+ slots,
+ store: createStore({
+ actions: {
+ setAddColumnFormVisibility: jest.fn(),
+ ...actions,
},
}),
- );
+ });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findSearchLabelFormGroup = () => wrapper.findComponent(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- it('shows form title & search input', () => {
+ it('shows form title', () => {
mountComponent();
- findDropdown().vm.$emit('show');
-
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
- expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => {
@@ -88,61 +60,6 @@ describe('Board card layout', () => {
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
- describe('items', () => {
- const mountWithItems = (loading) =>
- mountComponent({
- loading,
- slots: {
- items: '<div class="item-slot">Some kind of list</div>',
- },
- });
-
- it('hides items slot and shows skeleton while loading', () => {
- mountWithItems(true);
-
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- expect(wrapper.find('.item-slot').exists()).toBe(false);
- });
-
- it('shows items slot and hides skeleton while not loading', () => {
- mountWithItems(false);
-
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- expect(wrapper.find('.item-slot').exists()).toBe(true);
- });
- });
-
- describe('search box', () => {
- it('sets label and placeholder text from props', () => {
- const props = {
- searchLabel: 'Some items',
- searchPlaceholder: 'Search for an item',
- };
-
- mountComponent(props);
-
- expect(findSearchLabelFormGroup().attributes('label')).toEqual(props.searchLabel);
- expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder);
- });
-
- it('does not show the dropdown as invalid by default', () => {
- mountComponent();
-
- expect(findSearchLabelFormGroup().attributes('state')).toBe('true');
- expect(findDropdown().props('toggleClass')).not.toContain('gl-inset-border-1-red-400!');
- });
-
- it('emits filter event on input', () => {
- mountComponent();
-
- const searchText = 'some text';
-
- findSearchInput().vm.$emit('input', searchText);
-
- expect(wrapper.emitted('filter-items')).toEqual([[searchText]]);
- });
- });
-
describe('Add list button', () => {
it('is enabled by default', () => {
mountComponent();
@@ -159,16 +76,5 @@ describe('Board card layout', () => {
expect(wrapper.emitted('add-list')).toEqual([[]]);
});
-
- it('does not emit the add-list event on click and shows the dropdown as invalid when no ID is selected', async () => {
- mountComponent();
-
- await submitButton().vm.$emit('click');
-
- expect(findSearchLabelFormGroup().attributes('state')).toBeUndefined();
- expect(findDropdown().props('toggleClass')).toContain('gl-inset-border-1-red-400!');
-
- expect(wrapper.emitted('add-list')).toBeUndefined();
- });
});
});
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index a3b2988ce75..a09c3aaa55e 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -1,8 +1,7 @@
-import { GlFormRadioGroup } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
@@ -13,8 +12,9 @@ Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const selectLabel = (id) => {
- wrapper.findComponent(GlFormRadioGroup).vm.$emit('change', id);
+ findDropdown().vm.$emit('select', id);
};
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
@@ -34,33 +34,34 @@ describe('Board card layout', () => {
getListByLabelId = jest.fn(),
actions = {},
} = {}) => {
- wrapper = extendedWrapper(
- shallowMount(BoardAddNewColumn, {
- data() {
- return {
- selectedId,
- };
+ wrapper = shallowMountExtended(BoardAddNewColumn, {
+ data() {
+ return {
+ selectedId,
+ };
+ },
+ store: createStore({
+ actions: {
+ fetchLabels: jest.fn(),
+ setAddColumnFormVisibility: jest.fn(),
+ ...actions,
},
- store: createStore({
- actions: {
- fetchLabels: jest.fn(),
- setAddColumnFormVisibility: jest.fn(),
- ...actions,
- },
- getters: {
- getListByLabelId: () => getListByLabelId,
- },
- state: {
- labels,
- labelsLoading: false,
- },
- }),
- provide: {
- scopedLabelsAvailable: true,
- isEpicBoard: false,
+ getters: {
+ getListByLabelId: () => getListByLabelId,
+ },
+ state: {
+ labels,
+ labelsLoading: false,
},
}),
- );
+ provide: {
+ scopedLabelsAvailable: true,
+ isEpicBoard: false,
+ },
+ stubs: {
+ GlCollapsibleListbox,
+ },
+ });
// trigger change event
if (selectedId) {
@@ -68,10 +69,6 @@ describe('Board card layout', () => {
}
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Add list button', () => {
it('calls addList', async () => {
const getListByLabelId = jest.fn().mockReturnValue(null);
diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
index 354eb7bff16..d8b93e1f3b6 100644
--- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
@@ -17,7 +17,7 @@ describe('BoardAddNewColumnTrigger', () => {
const mountComponent = () => {
wrapper = mountExtended(BoardAddNewColumnTrigger, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
store: createStore(),
});
@@ -27,10 +27,6 @@ describe('BoardAddNewColumnTrigger', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when button is active', () => {
it('does not show the tooltip', () => {
const tooltip = findTooltipText();
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 12318fb5d16..148e696b57b 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -28,13 +28,12 @@ describe('BoardApp', () => {
store,
provide: {
initialBoardId: 'gid://gitlab/Board/1',
+ initialFilterParams: {},
},
});
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
store = null;
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 84e6318d98e..46116bed4cf 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -82,8 +82,6 @@ describe('Board card', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
store = null;
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index c0bb51620f2..011665eee68 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -10,11 +10,6 @@ describe('Board Column Component', () => {
let wrapper;
let store;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const initStore = () => {
store = createStore();
};
@@ -36,6 +31,7 @@ describe('Board Column Component', () => {
propsData: {
list: listMock,
boardId: 'gid://gitlab/Board/1',
+ filters: {},
},
provide: {
isApolloBoard: false,
diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js
index 6f0971a9458..d2948daf121 100644
--- a/spec/frontend/boards/components/board_configuration_options_spec.js
+++ b/spec/frontend/boards/components/board_configuration_options_spec.js
@@ -16,10 +16,6 @@ describe('BoardConfigurationOptions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const backlogListCheckbox = () => wrapper.find('[data-testid="backlog-list-checkbox"]');
const closedListCheckbox = () => wrapper.find('[data-testid="closed-list-checkbox"]');
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 955267a415c..90376a4a553 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -89,10 +89,6 @@ describe('BoardContentSidebar', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('confirms we render GlDrawer', () => {
expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 97596c86198..33351bf8efd 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -4,6 +4,8 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
+
+import eventHub from '~/boards/eventhub';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
@@ -24,7 +26,6 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
let fakeApollo;
- window.gon = {};
const defaultState = {
isShowingEpicsSwimlanes: false,
@@ -61,6 +62,8 @@ describe('BoardContent', () => {
apolloProvider: fakeApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
+ filterParams: {},
+ isSwimlanesOn: false,
...props,
},
provide: {
@@ -102,7 +105,6 @@ describe('BoardContent', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -203,5 +205,14 @@ describe('BoardContent', () => {
it('renders BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
+
+ it('refetches lists when updateBoard event is received', async () => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+
+ createComponent({ isApolloBoard: true });
+ await waitForPromises();
+
+ expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
+ });
});
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 4c0cc36889c..d8bc7f95f18 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -55,10 +55,10 @@ describe('BoardFilteredSearch', () => {
},
];
- const createComponent = ({ initialFilterParams = {}, props = {} } = {}) => {
+ const createComponent = ({ initialFilterParams = {}, props = {}, provide = {} } = {}) => {
store = createStore();
wrapper = shallowMount(BoardFilteredSearch, {
- provide: { initialFilterParams, fullPath: '' },
+ provide: { initialFilterParams, fullPath: '', isApolloBoard: false, ...provide },
store,
propsData: {
...props,
@@ -69,10 +69,6 @@ describe('BoardFilteredSearch', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -191,4 +187,24 @@ describe('BoardFilteredSearch', () => {
]);
});
});
+
+ describe('when Apollo boards FF is on', () => {
+ beforeEach(() => {
+ createComponent({ provide: { isApolloBoard: true } });
+ });
+
+ it('emits setFilters and updates URL when onFilter is emitted', () => {
+ jest.spyOn(urlUtility, 'updateHistory');
+
+ findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
+
+ expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ title: '',
+ replace: true,
+ url: 'http://test.host/',
+ });
+
+ expect(wrapper.emitted('setFilters')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index f8154145d43..62db59f8f57 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -10,12 +10,14 @@ import { formType } from '~/boards/constants';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
+import eventHub from '~/boards/eventhub';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
+jest.mock('~/boards/eventhub');
Vue.use(Vuex);
@@ -59,18 +61,14 @@ describe('BoardForm', () => {
},
});
- const createComponent = (props, data) => {
+ const createComponent = (props, provide) => {
wrapper = shallowMountExtended(BoardForm, {
propsData: { ...defaultProps, ...props },
- data() {
- return {
- ...data,
- };
- },
provide: {
boardBaseUrl: 'root',
isGroupBoard: true,
isProjectBoard: false,
+ ...provide,
},
mocks: {
$apollo: {
@@ -83,8 +81,6 @@ describe('BoardForm', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mutate = null;
});
@@ -140,7 +136,7 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Create board');
- expect(findModalActionPrimary().attributes[0].variant).toBe('confirm');
+ expect(findModalActionPrimary().attributes.variant).toBe('confirm');
});
it('does not render delete confirmation message', () => {
@@ -209,6 +205,30 @@ describe('BoardForm', () => {
expect(setBoardMock).not.toHaveBeenCalled();
expect(setErrorMock).toHaveBeenCalled();
});
+
+ describe('when Apollo boards FF is on', () => {
+ it('calls a correct GraphQL mutation and emits addBoard event when creating a board', async () => {
+ createComponent(
+ { canAdminBoard: true, currentPage: formType.new },
+ { isApolloBoard: true },
+ );
+ fillForm();
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: createBoardMutation,
+ variables: {
+ input: expect.objectContaining({
+ name: 'test',
+ }),
+ },
+ });
+
+ await waitForPromises();
+ expect(wrapper.emitted('addBoard')).toHaveLength(1);
+ });
+ });
});
});
@@ -228,7 +248,7 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Save changes');
- expect(findModalActionPrimary().attributes[0].variant).toBe('confirm');
+ expect(findModalActionPrimary().attributes.variant).toBe('confirm');
});
it('does not render delete confirmation message', () => {
@@ -308,13 +328,48 @@ describe('BoardForm', () => {
expect(setBoardMock).not.toHaveBeenCalled();
expect(setErrorMock).toHaveBeenCalled();
});
+
+ describe('when Apollo boards FF is on', () => {
+ it('calls a correct GraphQL mutation and emits updateBoard event when updating a board', async () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
+ },
+ });
+ setWindowLocation('https://test/boards/1');
+
+ createComponent(
+ { canAdminBoard: true, currentPage: formType.edit },
+ { isApolloBoard: true },
+ );
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: updateBoardMutation,
+ variables: {
+ input: expect.objectContaining({
+ id: currentBoard.id,
+ }),
+ },
+ });
+
+ await waitForPromises();
+ expect(eventHub.$emit).toHaveBeenCalledTimes(1);
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateBoard', {
+ id: 'gid://gitlab/Board/321',
+ webPath: 'test-path',
+ });
+ });
+ });
});
describe('when deleting a board', () => {
it('passes correct primary action text and variant', () => {
createComponent({ canAdminBoard: true, currentPage: formType.delete });
expect(findModalActionPrimary().text).toBe('Delete');
- expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
+ expect(findModalActionPrimary().attributes.variant).toBe('danger');
});
it('renders delete confirmation message', () => {
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 9e65e900440..466321cf1cc 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,10 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header.vue';
import { ListType } from '~/boards/constants';
@@ -22,8 +21,6 @@ describe('Board List Header Component', () => {
const toggleListCollapsedSpy = jest.fn();
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
fakeApollo = null;
localStorage.clear();
@@ -64,31 +61,34 @@ describe('Board List Header Component', () => {
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
- wrapper = extendedWrapper(
- shallowMount(BoardListHeader, {
- apolloProvider: fakeApollo,
- store,
- propsData: {
- list: listMock,
- },
- provide: {
- boardId,
- weightFeatureAvailable: false,
- currentUserId,
- isEpicBoard: false,
- disabled: false,
- ...injectedProps,
- },
- }),
- );
+ wrapper = shallowMountExtended(BoardListHeader, {
+ apolloProvider: fakeApollo,
+ store,
+ propsData: {
+ list: listMock,
+ filterParams: {},
+ },
+ provide: {
+ boardId,
+ weightFeatureAvailable: false,
+ currentUserId,
+ isEpicBoard: false,
+ disabled: false,
+ ...injectedProps,
+ },
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
+ });
};
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const isCollapsed = () => wrapper.vm.list.collapsed;
-
- const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' });
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
- const findSettingsButton = () => wrapper.findComponent({ ref: 'settingsBtn' });
+ const findNewIssueButton = () => wrapper.findByTestId('newIssueBtn');
+ const findSettingsButton = () => wrapper.findByTestId('settingsBtn');
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
@@ -100,59 +100,49 @@ describe('Board List Header Component', () => {
ListType.assignee,
];
- it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
+ it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findAddIssueButton().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it.each(hasAddButton)('does render when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findAddIssueButton().exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(true);
+ expect(findNewIssueButton().exists()).toBe(true);
});
- it('has a test for each list type', () => {
- createComponent();
-
- Object.values(ListType).forEach((value) => {
- expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
- });
- });
-
- it('does not render when logged out', () => {
+ it('does not render dropdown when logged out', () => {
createComponent({
currentUserId: null,
});
- expect(findAddIssueButton().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
});
describe('Settings Button', () => {
- describe('with disabled=true', () => {
- const hasSettings = [
- ListType.assignee,
- ListType.milestone,
- ListType.iteration,
- ListType.label,
- ];
- const hasNoSettings = [ListType.backlog, ListType.closed];
-
- it.each(hasSettings)('does render for List Type `%s` when disabled=true', (listType) => {
- createComponent({ listType, injectedProps: { disabled: true } });
-
- expect(findSettingsButton().exists()).toBe(true);
- });
+ const hasSettings = [ListType.assignee, ListType.milestone, ListType.iteration, ListType.label];
- it.each(hasNoSettings)(
- 'does not render for List Type `%s` when disabled=true',
- (listType) => {
- createComponent({ listType });
+ it.each(hasSettings)('does render for List Type `%s`', (listType) => {
+ createComponent({ listType });
- expect(findSettingsButton().exists()).toBe(false);
- },
- );
+ expect(findDropdown().exists()).toBe(true);
+ expect(findSettingsButton().exists()).toBe(true);
+ });
+
+ it('does not render dropdown when ListType `closed`', () => {
+ createComponent({ listType: ListType.closed });
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('renders dropdown but not the Settings button when ListType `backlog`', () => {
+ createComponent({ listType: ListType.backlog });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findSettingsButton().exists()).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index c3e69ba0e40..651d1daee52 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -51,10 +51,6 @@ describe('Issue boards new issue form', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders board-new-item component', () => {
const boardNewItem = findBoardNewItem();
expect(boardNewItem.exists()).toBe(true);
diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js
index f4e9901aad2..f11eb2baca7 100644
--- a/spec/frontend/boards/components/board_new_item_spec.js
+++ b/spec/frontend/boards/components/board_new_item_spec.js
@@ -35,10 +35,6 @@ describe('BoardNewItem', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe('when the user provides a valid input', () => {
it('finds an enabled create button', async () => {
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 7d602042685..d0928485caf 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -48,7 +48,7 @@ describe('BoardSettingsSidebar', () => {
isIssueBoard: true,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlDrawer: stubComponent(GlDrawer, {
@@ -65,8 +65,6 @@ describe('BoardSettingsSidebar', () => {
afterEach(() => {
jest.restoreAllMocks();
- wrapper.destroy();
- wrapper = null;
});
it('finds a MountingPortal component', () => {
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index 8258d9fe7f4..d97a1dbff47 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -11,7 +11,7 @@ import ConfigToggle from '~/boards/components/config_toggle.vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
-import { BoardType } from '~/boards/constants';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
@@ -43,8 +43,9 @@ describe('BoardTopBar', () => {
wrapper = shallowMount(BoardTopBar, {
store,
apolloProvider: mockApollo,
- props: {
+ propsData: {
boardId: 'gid://gitlab/Board/1',
+ isSwimlanesOn: false,
},
provide: {
swimlanesFeatureAvailable: false,
@@ -64,7 +65,6 @@ describe('BoardTopBar', () => {
};
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
});
@@ -96,6 +96,11 @@ describe('BoardTopBar', () => {
it('does not render BoardAddNewColumnTrigger component', () => {
expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(false);
});
+
+ it('emits setFilters when setFilters is emitted by filtered search', () => {
+ wrapper.findComponent(IssueBoardFilteredSearch).vm.$emit('setFilters');
+ expect(wrapper.emitted('setFilters')).toHaveLength(1);
+ });
});
describe('when user can admin list', () => {
@@ -111,14 +116,14 @@ describe('BoardTopBar', () => {
describe('Apollo boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
- ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
- ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ ${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
createComponent({
provide: {
boardType,
- isProjectBoard: boardType === BoardType.project,
- isGroupBoard: boardType === BoardType.group,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
isApolloBoard: true,
},
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 28f51e0ecbf..aa146eb4609 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -5,11 +5,11 @@ import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
-import { BoardType } from '~/boards/constants';
import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -116,7 +116,6 @@ describe('BoardsSelector', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -228,13 +227,13 @@ describe('BoardsSelector', () => {
describe('fetching all boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
- ${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
- ${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
+ ${WORKSPACE_GROUP} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
+ ${WORKSPACE_PROJECT} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
createStore();
createComponent({
- isGroupBoard: boardType === BoardType.group,
- isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
});
await nextTick();
diff --git a/spec/frontend/boards/components/config_toggle_spec.js b/spec/frontend/boards/components/config_toggle_spec.js
index 47d4692453d..5330721451e 100644
--- a/spec/frontend/boards/components/config_toggle_spec.js
+++ b/spec/frontend/boards/components/config_toggle_spec.js
@@ -23,10 +23,6 @@ describe('ConfigToggle', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a button with label `View scope` when `canAdminList` is `false`', () => {
wrapper = createComponent({ canAdminList: false });
expect(findButton().text()).toBe('View scope');
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index 57a30ddc512..5b5b68d5dbe 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -2,10 +2,10 @@ import { orderBy } from 'lodash';
import { shallowMount } from '@vue/test-utils';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue';
-import issueBoardFilters from '~/boards/issue_board_filters';
+import issueBoardFilters from 'ee_else_ce/boards/issue_board_filters';
import { mockTokens } from '../mock_data';
-jest.mock('~/boards/issue_board_filters');
+jest.mock('ee_else_ce/boards/issue_board_filters');
describe('IssueBoardFilter', () => {
let wrapper;
@@ -14,6 +14,9 @@ describe('IssueBoardFilter', () => {
const createComponent = ({ isSignedIn = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, {
+ propsData: {
+ boardId: 'gid://gitlab/Board/1',
+ },
provide: {
isSignedIn,
releasesFetchPath: '/releases',
@@ -35,10 +38,6 @@ describe('IssueBoardFilter', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -48,6 +47,11 @@ describe('IssueBoardFilter', () => {
expect(findBoardsFilteredSearch().exists()).toBe(true);
});
+ it('emits setFilters when setFilters is emitted', () => {
+ findBoardsFilteredSearch().vm.$emit('setFilters');
+ expect(wrapper.emitted('setFilters')).toHaveLength(1);
+ });
+
it.each`
isSignedIn
${true}
diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js
index 45fa10bf03a..dee8febfe4d 100644
--- a/spec/frontend/boards/components/issue_due_date_spec.js
+++ b/spec/frontend/boards/components/issue_due_date_spec.js
@@ -20,10 +20,6 @@ describe('Issue Due Date component', () => {
date = new Date();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render "Today" if the due date is today', () => {
wrapper = createComponent();
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 948a7a20f7f..42507ef560b 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -7,10 +7,6 @@ describe('Issue Time Estimate component', () => {
const findIssueTimeEstimate = () => wrapper.find('[data-testid="issue-time-estimate"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when limitToHours is false', () => {
beforeEach(() => {
wrapper = shallowMount(IssueTimeEstimate, {
diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js
index 0c0c7f66933..f2cc8eb1167 100644
--- a/spec/frontend/boards/components/item_count_spec.js
+++ b/spec/frontend/boards/components/item_count_spec.js
@@ -41,10 +41,6 @@ describe('IssueCount', () => {
createComponent({ maxIssueCount, itemsSize });
});
- afterEach(() => {
- vm.destroy();
- });
-
it('contains issueSize in the template', () => {
expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize));
});
@@ -66,10 +62,6 @@ describe('IssueCount', () => {
createComponent({ maxIssueCount, itemsSize });
});
- afterEach(() => {
- vm.destroy();
- });
-
it('contains issueSize in the template', () => {
expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize));
});
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
index 5e2222ac3d7..6dbeac3864f 100644
--- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -21,11 +21,6 @@ describe('boards sidebar remove issue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('renders title', () => {
const title = 'Sidebar item title';
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
index e2e4baefad0..b01ee01120e 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -37,11 +37,6 @@ describe('BoardSidebarTimeTracker', () => {
store.state.activeId = '1';
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
timeTrackingLimitToHours | canUpdate
${true} | ${false}
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index bc66a0515aa..a20884baf3b 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -27,9 +27,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
store = null;
- wrapper = null;
});
const createWrapper = (item = TEST_ISSUE_A) => {
diff --git a/spec/frontend/boards/components/toggle_focus_spec.js b/spec/frontend/boards/components/toggle_focus_spec.js
index 3cbaac91f8d..cad287954d7 100644
--- a/spec/frontend/boards/components/toggle_focus_spec.js
+++ b/spec/frontend/boards/components/toggle_focus_spec.js
@@ -10,7 +10,7 @@ describe('ToggleFocus', () => {
const createComponent = () => {
wrapper = shallowMountExtended(ToggleFocus, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
attachTo: document.body,
});
@@ -18,10 +18,6 @@ describe('ToggleFocus', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a button with `maximize` icon', () => {
createComponent();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 1d011eacf1c..e5167120542 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -477,6 +477,9 @@ export const mockList = {
loading: false,
issuesCount: 1,
maxIssueCount: 0,
+ metadata: {
+ epicsCount: 1,
+ },
__typename: 'BoardList',
};
@@ -915,6 +918,7 @@ export const epicBoardListQueryResponse = (totalWeight = 5) => ({
__typename: 'EpicList',
id: 'gid://gitlab/Boards::EpicList/3',
metadata: {
+ epicsCount: 1,
totalWeight,
},
},
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index 4324e7068e0..74ce4b6b786 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -71,11 +71,6 @@ describe('ProjectSelect component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays a header title', () => {
createWrapper();
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index ab959abaa99..f430062bb73 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -2,13 +2,7 @@ import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
-import {
- inactiveId,
- ISSUABLE,
- ListType,
- BoardType,
- DraggableItemTypes,
-} from 'ee_else_ce/boards/constants';
+import { inactiveId, ISSUABLE, ListType, DraggableItemTypes } from 'ee_else_ce/boards/constants';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import testAction from 'helpers/vuex_action_helper';
import {
@@ -26,7 +20,7 @@ import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
@@ -49,7 +43,7 @@ import {
mockMilestones,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
// We need this helper to make sure projectPath is including
// subgroups when the movIssue action is called.
@@ -300,8 +294,8 @@ describe('fetchLists', () => {
it.each`
issuableType | boardType | fullBoardId | isGroup | isProject
- ${TYPE_ISSUE} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
- ${TYPE_ISSUE} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
+ ${TYPE_ISSUE} | ${WORKSPACE_GROUP} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
+ ${TYPE_ISSUE} | ${WORKSPACE_PROJECT} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
`(
'calls $issuableType query with correct variables',
async ({ issuableType, boardType, fullBoardId, isGroup, isProject }) => {
@@ -336,7 +330,7 @@ describe('fetchLists', () => {
describe('fetchMilestones', () => {
const queryResponse = {
data: {
- project: {
+ workspace: {
milestones: {
nodes: mockMilestones,
},
@@ -346,7 +340,7 @@ describe('fetchMilestones', () => {
const queryErrors = {
data: {
- project: {
+ workspace: {
errors: ['You cannot view these milestones'],
milestones: {},
},
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index c86a256bd96..944a7493504 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -31,10 +31,6 @@ describe('Boards - Getters', () => {
});
describe('isSwimlanesOn', () => {
- afterEach(() => {
- window.gon = { features: {} };
- });
-
it('returns false', () => {
expect(getters.isSwimlanesOn()).toBe(false);
});
@@ -171,10 +167,6 @@ describe('Boards - Getters', () => {
});
describe('isEpicBoard', () => {
- afterEach(() => {
- window.gon = { features: {} };
- });
-
it('returns false', () => {
expect(getters.isEpicBoard()).toBe(false);
});
diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js
index b029f34c3d7..5b2ec443c59 100644
--- a/spec/frontend/branches/components/delete_branch_button_spec.js
+++ b/spec/frontend/branches/components/delete_branch_button_spec.js
@@ -25,10 +25,6 @@ describe('Delete branch button', () => {
eventHubSpy = jest.spyOn(eventHub, '$emit');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the button with default tooltip, style, and icon', () => {
createComponent();
diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js
index c977868ca93..dd5b7fca564 100644
--- a/spec/frontend/branches/components/delete_branch_modal_spec.js
+++ b/spec/frontend/branches/components/delete_branch_modal_spec.js
@@ -52,10 +52,6 @@ describe('Delete branch modal', () => {
const expectedUnmergedWarning =
"This branch hasn't been merged into default. To avoid data loss, consider merging this branch before deleting it.";
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Deleting a regular branch', () => {
const expectedTitle = 'Delete branch. Are you ABSOLUTELY SURE?';
const expectedWarning = "You're about to permanently delete the branch test_modal.";
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
index 4f1e772f4a4..75a669c78f2 100644
--- a/spec/frontend/branches/components/delete_merged_branches_spec.js
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -27,7 +27,7 @@ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
...propsDataMock,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs,
});
@@ -78,10 +78,6 @@ describe('Delete merged branches component', () => {
createComponent(shallowMountExtended, stubsData);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders correct modal title and text', () => {
const modalText = findModal().text();
expect(findModal().props('title')).toBe(i18n.modalTitle);
diff --git a/spec/frontend/branches/components/divergence_graph_spec.js b/spec/frontend/branches/components/divergence_graph_spec.js
index 9429a6e982c..66193c2ebf0 100644
--- a/spec/frontend/branches/components/divergence_graph_spec.js
+++ b/spec/frontend/branches/components/divergence_graph_spec.js
@@ -9,10 +9,6 @@ function factory(propsData = {}) {
}
describe('Branch divergence graph component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it('renders ahead and behind count', () => {
factory({
defaultBranch: 'main',
diff --git a/spec/frontend/branches/components/graph_bar_spec.js b/spec/frontend/branches/components/graph_bar_spec.js
index 61c051b49c6..585b376081b 100644
--- a/spec/frontend/branches/components/graph_bar_spec.js
+++ b/spec/frontend/branches/components/graph_bar_spec.js
@@ -8,10 +8,6 @@ function factory(propsData = {}) {
}
describe('Branch divergence graph bar component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
position | positionClass
${'left'} | ${'position-right-0'}
diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js
index 20e69b5a834..6d6d8043797 100644
--- a/spec/frontend/captcha/captcha_modal_spec.js
+++ b/spec/frontend/captcha/captcha_modal_spec.js
@@ -1,6 +1,5 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import CaptchaModal from '~/captcha/captcha_modal.vue';
import { initRecaptchaScript } from '~/captcha/init_recaptcha_script';
@@ -9,10 +8,11 @@ jest.mock('~/captcha/init_recaptcha_script');
describe('Captcha Modal', () => {
let wrapper;
- let modal;
let grecaptcha;
const captchaSiteKey = 'abc123';
+ const showSpy = jest.fn();
+ const hideSpy = jest.fn();
function createComponent({ props = {} } = {}) {
wrapper = shallowMount(CaptchaModal, {
@@ -21,11 +21,18 @@ describe('Captcha Modal', () => {
...props,
},
stubs: {
- GlModal: stubComponent(GlModal),
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showSpy,
+ hide: hideSpy,
+ },
+ }),
},
});
}
+ const findGlModal = () => wrapper.findComponent(GlModal);
+
beforeEach(() => {
grecaptcha = {
render: jest.fn(),
@@ -34,38 +41,17 @@ describe('Captcha Modal', () => {
initRecaptchaScript.mockResolvedValue(grecaptcha);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findGlModal = () => {
- const glModal = wrapper.findComponent(GlModal);
-
- jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown'));
- jest
- .spyOn(glModal.vm, 'hide')
- .mockImplementation(() => glModal.vm.$emit('hide', { trigger: '' }));
-
- return glModal;
- };
-
- const showModal = () => {
- wrapper.setProps({ needsCaptchaResponse: true });
- };
-
- beforeEach(() => {
- createComponent();
- modal = findGlModal();
- });
-
describe('rendering', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders', () => {
- expect(modal.exists()).toBe(true);
+ expect(findGlModal().exists()).toBe(true);
});
it('assigns the modal a unique ID', () => {
- const firstInstanceModalId = modal.props('modalId');
+ const firstInstanceModalId = findGlModal().props('modalId');
createComponent();
const secondInstanceModalId = findGlModal().props('modalId');
expect(firstInstanceModalId).not.toEqual(secondInstanceModalId);
@@ -76,13 +62,12 @@ describe('Captcha Modal', () => {
describe('when modal is shown', () => {
describe('when initRecaptchaScript promise resolves successfully', () => {
beforeEach(async () => {
- showModal();
-
- await nextTick();
+ createComponent({ props: { needsCaptchaResponse: true } });
+ findGlModal().vm.$emit('shown');
});
it('shows modal', async () => {
- expect(findGlModal().vm.show).toHaveBeenCalled();
+ expect(showSpy).toHaveBeenCalled();
});
it('renders window.grecaptcha', () => {
@@ -108,7 +93,7 @@ describe('Captcha Modal', () => {
it('hides modal with null trigger', async () => {
// Assert that hide is called with zero args, so that we don't trigger the logic
// for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
- expect(modal.vm.hide).toHaveBeenCalledWith();
+ expect(hideSpy).toHaveBeenCalledWith();
});
});
@@ -127,7 +112,7 @@ describe('Captcha Modal', () => {
const bvModalEvent = {
trigger,
};
- modal.vm.$emit('hide', bvModalEvent);
+ findGlModal().vm.$emit('hide', bvModalEvent);
});
it(`emits receivedCaptchaResponse with ${JSON.stringify(expected)}`, () => {
@@ -141,21 +126,24 @@ describe('Captcha Modal', () => {
const fakeError = {};
beforeEach(() => {
- initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError));
+ createComponent({
+ props: { needsCaptchaResponse: true },
+ });
+ initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError));
jest.spyOn(console, 'error').mockImplementation();
- showModal();
+ findGlModal().vm.$emit('shown');
});
it('emits receivedCaptchaResponse exactly once with null', () => {
expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[null]]);
});
- it('hides modal with null trigger', async () => {
+ it('hides modal with null trigger', () => {
// Assert that hide is called with zero args, so that we don't trigger the logic
// for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
- expect(modal.vm.hide).toHaveBeenCalledWith();
+ expect(hideSpy).toHaveBeenCalledWith();
});
it('calls console.error with a message and the exception', () => {
diff --git a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
index d4f588a0e09..4b7ca36f331 100644
--- a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
@@ -48,7 +48,6 @@ describe('CI Lint', () => {
afterEach(() => {
mockMutate.mockClear();
- wrapper.destroy();
});
it('displays the editor', () => {
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
index 5e0c35c9f90..8e012883f09 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
@@ -16,10 +16,6 @@ describe('Ci Project Variable wrapper', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
areScopedVariablesAvailable: false,
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
index 2fd395a1230..7181398c2a6 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -27,10 +27,6 @@ describe('Ci environments dropdown', () => {
findListbox().vm.$emit('search', searchTerm);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No environments found', () => {
beforeEach(() => {
createComponent({ searchTerm: 'stable' });
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index c0fb133b9b1..77d90a7667d 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -24,10 +24,6 @@ describe('Ci Group Variable wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Props', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index bd1e6b17d6b..ce5237a84f7 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -25,10 +25,6 @@ describe('Ci Project Variable wrapper', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId),
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 508af964ca3..8f3fccc2804 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -85,10 +85,6 @@ describe('Ci variable modal', () => {
const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Adding a variable', () => {
describe('when no key/value pair are present', () => {
beforeEach(() => {
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 32af2ec4de9..0141232a299 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -22,6 +22,7 @@ describe('Ci variable table', () => {
hideEnvironmentScope: false,
isLoading: false,
maxVariableLimit: 5,
+ pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
};
@@ -37,10 +38,6 @@ describe('Ci variable table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('props passing', () => {
it('passes props down correctly to the ci table', () => {
createComponent();
@@ -49,6 +46,7 @@ describe('Ci variable table', () => {
entity: 'project',
isLoading: defaultProps.isLoading,
maxVariableLimit: defaultProps.maxVariableLimit,
+ pageInfo: defaultProps.pageInfo,
variables: defaultProps.variables,
});
});
@@ -144,4 +142,22 @@ describe('Ci variable table', () => {
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
});
});
+
+ describe('pages events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ eventName | args
+ ${'handle-prev-page'} | ${undefined}
+ ${'handle-next-page'} | ${undefined}
+ ${'sort-changed'} | ${{ sortDesc: true }}
+ `('bubbles up the $eventName event', async ({ args, eventName }) => {
+ findCiVariableTable().vm.$emit(eventName, args);
+ await nextTick();
+
+ expect(wrapper.emitted(eventName)).toEqual([[args]]);
+ });
+ });
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index c977ae773db..87192006efc 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -41,7 +41,7 @@ import {
mockAdminVariables,
} from '../mocks';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -53,6 +53,7 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
+ pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
};
@@ -105,345 +106,378 @@ describe('Ci Variable Shared Component', () => {
mockVariables = jest.fn();
});
- 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('successfully', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo({ provide: createProjectProvide() });
+ describe.each`
+ isVariablePagesEnabled | text
+ ${true} | ${'enabled'}
+ ${false} | ${'disabled'}
+ `('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
+ const featureFlagProvide = isVariablePagesEnabled
+ ? { glFeatures: { ciVariablesPages: true } }
+ : {};
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
});
- it('passes down the expected max variable limit as props', () => {
- expect(findCiSettings().props('maxVariableLimit')).toBe(
- mockProjectVariables.data.project.ciVariables.limit,
- );
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
});
+ });
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
- });
+ describe('when queries are resolved', () => {
+ describe('successfully', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockProjectVariables.data.project.ciVariables.nodes,
- );
- });
+ await createComponentWithApollo({
+ provide: { ...createProjectProvide(), ...featureFlagProvide },
+ });
+ });
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
+ it('passes down the expected max variable limit as props', () => {
+ expect(findCiSettings().props('maxVariableLimit')).toBe(
+ mockProjectVariables.data.project.ciVariables.limit,
+ );
+ });
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockRejectedValue();
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
+ });
- await createComponentWithApollo();
- });
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockProjectVariables.data.project.ciVariables.nodes,
+ );
+ });
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
- });
- describe('with an error for environments', () => {
- beforeEach(async () => {
- mockEnvironments.mockRejectedValue();
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockRejectedValue();
- await createComponentWithApollo();
- });
+ await createComponentWithApollo({ provide: featureFlagProvide });
+ });
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
});
- });
- });
- describe('environment query', () => {
- describe('when there is an environment key in queryData', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('with an error for environments', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockRejectedValue();
+ mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo({ props: { ...createProjectProps() } });
- });
+ await createComponentWithApollo({ provide: featureFlagProvide });
+ });
- it('is executed', () => {
- expect(mockVariables).toHaveBeenCalled();
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ });
});
});
- describe('when there isnt an environment key in queryData', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ describe('environment query', () => {
+ describe('when there is an environment key in queryData', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo({ props: { ...createGroupProps() } });
- });
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: featureFlagProvide,
+ });
+ });
- it('is skipped', () => {
- expect(mockVariables).not.toHaveBeenCalled();
+ it('is executed', () => {
+ expect(mockVariables).toHaveBeenCalled();
+ });
});
- });
- });
- describe('mutations', () => {
- const groupProps = createGroupProps();
+ describe('when there isnt an environment key in queryData', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ provide: featureFlagProvide,
+ });
+ });
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: groupProps,
+ it('is skipped', () => {
+ expect(mockVariables).not.toHaveBeenCalled();
+ });
});
});
- it.each`
- actionName | mutation | event
- ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
- ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
- ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
- `(
- 'calls the right mutation from propsData 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: groupProps.fullPath,
- id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error on failure 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(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
+ describe('mutations', () => {
+ const groupProps = createGroupProps();
- describe('without fullpath and ID props', () => {
beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
+ mockVariables.mockResolvedValue(mockGroupVariables);
await createComponentWithApollo({
- customHandlers: [[getAdminVariables, mockVariables]],
- props: createInstanceProps(),
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: groupProps,
+ provide: featureFlagProvide,
});
});
-
- it('does not pass fullPath and ID to the mutation', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
-
- await findCiSettings().vm.$emit('add-variable', newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
- variables: {
- endpoint: mockProvide.endpoint,
- variable: newVariable,
- },
- });
- });
- });
- });
-
- describe('Props', () => {
- const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
- const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
-
- describe('in a specific context as', () => {
it.each`
- name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
- ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
- ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
- ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
+ actionName | mutation | event
+ ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
+ ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
+ ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
`(
- 'passes down all the required props when its a $name component',
- async ({
- mutation,
- maxVariableLimit,
- mockVariablesValue,
- mockEnvironmentsValue,
- withEnvironments,
- expectedEnvironments,
- propsFn,
- provideFn,
- }) => {
- const props = propsFn();
- const provide = provideFn();
-
- mockVariables.mockResolvedValue(mockVariablesValue);
-
- if (withEnvironments) {
- mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
- }
-
- let customHandlers = null;
-
- if (mutation) {
- customHandlers = [[mutation, mockVariables]];
- }
+ 'calls the right mutation from propsData 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: groupProps.fullPath,
+ id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
+ variable: newVariable,
+ },
+ });
+ },
+ );
- await createComponentWithApollo({ customHandlers, props, provide });
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
- expect(findCiSettings().props()).toEqual({
- areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
- hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- isLoading: false,
- maxVariableLimit,
- variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
- entity: props.entity,
- environments: expectedEnvironments,
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error on failure 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(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
},
);
- });
- describe('refetchAfterMutation', () => {
- it.each`
- bool | text
- ${true} | ${'refetches the variables'}
- ${false} | ${'does not refetch the variables'}
- `('when $bool it $text', async ({ bool }) => {
- await createComponentWithApollo({
- props: { ...createInstanceProps(), refetchAfterMutation: bool },
- });
+ describe('without fullpath and ID props', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
- jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn());
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ props: createInstanceProps(),
+ provide: featureFlagProvide,
+ });
+ });
- await findCiSettings().vm.$emit('add-variable', newVariable);
+ it('does not pass fullPath and ID to the mutation', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await nextTick();
+ await findCiSettings().vm.$emit('add-variable', newVariable);
- if (bool) {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
- } else {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
- }
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
+ variables: {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ });
+ });
});
});
- describe('Validators', () => {
- describe('queryData', () => {
- let error;
+ describe('Props', () => {
+ const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
+ const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+
+ describe('in a specific context as', () => {
+ it.each`
+ name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
+ ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
+ ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
+ ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
+ `(
+ 'passes down all the required props when its a $name component',
+ async ({
+ mutation,
+ maxVariableLimit,
+ mockVariablesValue,
+ mockEnvironmentsValue,
+ withEnvironments,
+ expectedEnvironments,
+ propsFn,
+ provideFn,
+ }) => {
+ const props = propsFn();
+ const provide = provideFn();
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
- });
+ mockVariables.mockResolvedValue(mockVariablesValue);
+
+ if (withEnvironments) {
+ mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
+ }
+
+ let customHandlers = null;
+
+ if (mutation) {
+ customHandlers = [[mutation, mockVariables]];
+ }
- it('will mount component with right data', async () => {
- try {
await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps() },
+ customHandlers,
+ props,
+ provide: { ...provide, ...featureFlagProvide },
});
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(true);
- expect(error).toBeUndefined();
- }
- });
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps(), queryData: { wrongKey: {} } },
+ expect(findCiSettings().props()).toEqual({
+ areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ pageInfo: defaultProps.pageInfo,
+ isLoading: false,
+ maxVariableLimit,
+ variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)
+ ?.nodes,
+ entity: props.entity,
+ environments: expectedEnvironments,
});
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
+ },
+ );
+ });
+
+ describe('refetchAfterMutation', () => {
+ it.each`
+ bool | text
+ ${true} | ${'refetches the variables'}
+ ${false} | ${'does not refetch the variables'}
+ `('when $bool it $text', async ({ bool }) => {
+ await createComponentWithApollo({
+ props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ provide: featureFlagProvide,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
+ jest
+ .spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch')
+ .mockImplementation(jest.fn());
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+
+ await nextTick();
+
+ if (bool) {
+ expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
+ } else {
+ expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
}
});
});
- describe('mutationData', () => {
- let error;
+ describe('Validators', () => {
+ describe('queryData', () => {
+ let error;
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
- });
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
- it('will mount component with right data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps() },
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(true);
- expect(error).toBeUndefined();
- }
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps() },
+ provide: featureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('will not mount component with wrong data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps(), queryData: { wrongKey: {} } },
+ provide: featureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(false);
+ expect(error.toString()).toContain('custom validator check failed for prop');
+ }
+ });
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ describe('mutationData', () => {
+ let error;
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ provide: featureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('will not mount component with wrong data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
+ provide: featureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(false);
+ expect(error.toString()).toContain('custom validator check failed for prop');
+ }
+ });
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
index 9e2508c56ee..2ef789e89c3 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
@@ -12,18 +12,25 @@ describe('Ci variable table', () => {
entity: 'project',
isLoading: false,
maxVariableLimit: mockVariables(projectString).length + 1,
+ pageInfo: {},
variables: mockVariables(projectString),
};
const mockMaxVariableLimit = defaultProps.variables.length;
- const createComponent = ({ props = {} } = {}) => {
+ const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = mountExtended(CiVariableTable, {
attachTo: document.body,
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciVariablesPages: false,
+ },
+ ...provide,
+ },
});
};
@@ -41,132 +48,136 @@ describe('Ci variable table', () => {
return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit });
};
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('When table is empty', () => {
- beforeEach(() => {
- createComponent({ props: { variables: [] } });
- });
+ describe.each`
+ isVariablePagesEnabled | text
+ ${true} | ${'enabled'}
+ ${false} | ${'disabled'}
+ `('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
+ const provide = isVariablePagesEnabled ? { glFeatures: { ciVariablesPages: true } } : {};
- it('displays empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
- });
-
- it('hides the reveal button', () => {
- expect(findRevealButton().exists()).toBe(false);
- });
- });
+ describe('When table is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { variables: [] }, provide });
+ });
- describe('When table has variables', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('displays empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
+ });
- it('does not display the empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ it('hides the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ });
});
- it('displays the reveal button', () => {
- expect(findRevealButton().exists()).toBe(true);
- });
+ describe('When table has variables', () => {
+ beforeEach(() => {
+ createComponent({ provide });
+ });
- it('displays the correct amount of variables', async () => {
- expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
- });
+ it('does not display the empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ });
- it('displays the correct variable options', async () => {
- expect(findOptionsValues(0)).toBe('Protected, Expanded');
- expect(findOptionsValues(1)).toBe('Masked');
- });
+ it('displays the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(true);
+ });
- it('enables the Add Variable button', () => {
- expect(findAddButton().props('disabled')).toBe(false);
- });
- });
+ it('displays the correct amount of variables', async () => {
+ expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
+ });
- describe('When variables have exceeded the max limit', () => {
- beforeEach(() => {
- createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } });
- });
+ it('displays the correct variable options', async () => {
+ expect(findOptionsValues(0)).toBe('Protected, Expanded');
+ expect(findOptionsValues(1)).toBe('Masked');
+ });
- it('disables the Add Variable button', () => {
- expect(findAddButton().props('disabled')).toBe(true);
+ it('enables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(false);
+ });
});
- });
- describe('max limit reached alert', () => {
- describe('when there is no variable limit', () => {
+ describe('When variables have exceeded the max limit', () => {
beforeEach(() => {
createComponent({
- props: { maxVariableLimit: 0 },
+ props: { maxVariableLimit: mockVariables(projectString).length },
+ provide,
});
});
- it('hides alert', () => {
- expect(findLimitReachedAlerts().length).toBe(0);
+ it('disables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
});
});
- describe('when variable limit exists', () => {
- it('hides alert when limit has not been reached', () => {
- createComponent();
+ describe('max limit reached alert', () => {
+ describe('when there is no variable limit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { maxVariableLimit: 0 },
+ provide,
+ });
+ });
- expect(findLimitReachedAlerts().length).toBe(0);
+ it('hides alert', () => {
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
});
- it('shows alert when limit has been reached', () => {
- const exceedsVariableLimitText = generateExceedsVariableLimitText(
- defaultProps.entity,
- defaultProps.variables.length,
- mockMaxVariableLimit,
- );
+ describe('when variable limit exists', () => {
+ it('hides alert when limit has not been reached', () => {
+ createComponent({ provide });
- createComponent({
- props: { maxVariableLimit: mockMaxVariableLimit },
+ expect(findLimitReachedAlerts().length).toBe(0);
});
- expect(findLimitReachedAlerts().length).toBe(2);
+ it('shows alert when limit has been reached', () => {
+ const exceedsVariableLimitText = generateExceedsVariableLimitText(
+ defaultProps.entity,
+ defaultProps.variables.length,
+ mockMaxVariableLimit,
+ );
+
+ createComponent({
+ props: { maxVariableLimit: mockMaxVariableLimit },
+ });
- expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
- expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+ expect(findLimitReachedAlerts().length).toBe(2);
- expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
- expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+
+ expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ });
});
});
- });
- describe('Table click actions', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('Table click actions', () => {
+ beforeEach(() => {
+ createComponent({ provide });
+ });
- it('reveals secret values when button is clicked', async () => {
- expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
- expect(findRevealedValues()).toHaveLength(0);
+ it('reveals secret values when button is clicked', async () => {
+ expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
+ expect(findRevealedValues()).toHaveLength(0);
- await findRevealButton().trigger('click');
+ await findRevealButton().trigger('click');
- expect(findHiddenValues()).toHaveLength(0);
- expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
- });
+ expect(findHiddenValues()).toHaveLength(0);
+ expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
+ });
- it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
- await findEditButton().trigger('click');
+ it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
+ await findEditButton().trigger('click');
- expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
- });
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
+ });
- it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
- await findAddButton().trigger('click');
+ it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
+ await findAddButton().trigger('click');
- expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ });
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
index b00e1adab63..48a85eba433 100644
--- a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
@@ -41,10 +41,6 @@ describe('EE - CodeSnippetAlert', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it("provides a link to the feature's documentation", () => {
const docsLink = findDocsLink();
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
index 8e1d8081dd8..b2dfa900b1d 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
@@ -33,10 +33,6 @@ describe('Pipeline Editor | Commit Form', () => {
const findSubmitBtn = () => wrapper.find('[type="submit"]');
const findCancelBtn = () => wrapper.find('[type="reset"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the form is displayed', () => {
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index f6e93c55bbb..f8be035d33c 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -113,10 +113,6 @@ describe('Pipeline Editor | Commit section', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the user commits a new file', () => {
beforeEach(async () => {
mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index 137137ec657..0ecb77674d5 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -21,10 +21,6 @@ describe('First pipeline card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
index cdce757ce7c..417597eaf1f 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
@@ -12,10 +12,6 @@ describe('Getting started card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 6909916c3e6..5399924b462 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -33,10 +33,6 @@ describe('Pipeline config reference card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
index 0c6879020de..547ba3cbd8b 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
@@ -12,10 +12,6 @@ describe('Visual and Lint card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 42e372cc1db..b07d63dd5d9 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -11,10 +11,6 @@ describe('Pipeline editor drawer', () => {
wrapper = shallowMount(PipelineEditorDrawer);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits close event when closing the drawer', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
index f510c61ee74..b0c889cfc9f 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
@@ -17,10 +17,6 @@ describe('Demo job pill', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the jobName', () => {
expect(wrapper.text()).toContain(jobName);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
index 2a2bc2547cc..2182b6e9cc6 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -34,10 +34,6 @@ describe('Text editor component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findEditor = () => wrapper.findComponent(MockSourceEditor);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when status is valid', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index dc72694d26f..560e8840d57 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -26,7 +26,6 @@ describe('CI Editor Header', () => {
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
- wrapper.destroy();
unmockTracking();
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
index ec987be8cb8..0be26570fbf 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
@@ -1,7 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { editor as monacoEditor } from 'monaco-editor';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
import { EDITOR_READY_EVENT } from '~/editor/constants';
+import { CiSchemaExtension as MockedCiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import { SOURCE_EDITOR_DEBOUNCE } from '~/ci/pipeline_editor/constants';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
import TextEditor from '~/ci/pipeline_editor/components/editor/text_editor.vue';
import {
mockCiConfigPath,
@@ -12,19 +16,26 @@ import {
mockDefaultBranch,
} from '../../mock_data';
+jest.mock('monaco-editor');
+jest.mock('~/editor/extensions/source_editor_ci_schema_ext', () => {
+ const { createMockSourceEditorExtension } = jest.requireActual(
+ 'helpers/create_mock_source_editor_extension',
+ );
+ const { CiSchemaExtension } = jest.requireActual(
+ '~/editor/extensions/source_editor_ci_schema_ext',
+ );
+
+ return {
+ CiSchemaExtension: createMockSourceEditorExtension(CiSchemaExtension),
+ };
+});
+
describe('Pipeline Editor | Text editor component', () => {
let wrapper;
let editorReadyListener;
- let mockUse;
- let mockRegisterCiSchema;
- let mockEditorInstance;
- let editorInstanceDetail;
-
- const MockSourceEditor = {
- template: '<div/>',
- props: ['value', 'fileName', 'editorOptions', 'debounceValue'],
- };
+
+ const getMonacoEditor = () => monacoEditor.create.mock.results[0].value;
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
@@ -44,33 +55,17 @@ describe('Pipeline Editor | Text editor component', () => {
[EDITOR_READY_EVENT]: editorReadyListener,
},
stubs: {
- SourceEditor: MockSourceEditor,
+ SourceEditor,
},
});
};
- const findEditor = () => wrapper.findComponent(MockSourceEditor);
+ const findEditor = () => wrapper.findComponent(SourceEditor);
beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
- mockEditorInstance = {
- use: mockUse,
- registerCiSchema: mockRegisterCiSchema,
- };
- editorInstanceDetail = {
- detail: {
- instance: mockEditorInstance,
- },
- };
- });
+ jest.spyOn(monacoEditor, 'create');
- afterEach(() => {
- wrapper.destroy();
-
- mockUse.mockClear();
- mockRegisterCiSchema.mockClear();
+ editorReadyListener = jest.fn();
});
describe('template', () => {
@@ -99,21 +94,34 @@ describe('Pipeline Editor | Text editor component', () => {
});
it('bubbles up events', () => {
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
-
expect(editorReadyListener).toHaveBeenCalled();
});
+
+ it('scrolls editor to bottom on scroll editor to bottom event', () => {
+ const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop');
+
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ expect(setScrollTop).toHaveBeenCalledWith(getMonacoEditor().getScrollHeight());
+ });
+
+ it('when destroyed, destroys scroll listener', () => {
+ const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop');
+
+ wrapper.destroy();
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ expect(setScrollTop).not.toHaveBeenCalled();
+ });
});
describe('CI schema', () => {
beforeEach(() => {
createComponent();
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('configures editor with syntax highlight', () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
+ expect(MockedCiSchemaExtension.mockedMethods.registerCiSchema).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index a26232df58f..bf14f4c4cd6 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -133,10 +133,6 @@ describe('Pipeline editor branch switcher', () => {
mockAvailableBranchQuery = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const testErrorHandling = () => {
expect(wrapper.emitted('showError')).toBeDefined();
expect(wrapper.emitted('showError')[0]).toEqual([
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index 907db16913c..19c113689c2 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -48,10 +48,6 @@ describe('Pipeline editor file nav', () => {
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findPopoverContainer = () => wrapper.findComponent(FileTreePopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
index 11ba517e0eb..306dd78d395 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
@@ -22,7 +22,7 @@ describe('Pipeline editor file nav', () => {
includes,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs,
}),
@@ -35,7 +35,6 @@ describe('Pipeline editor file nav', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('template', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
index bceb741f91c..80737e9a8ab 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
@@ -18,10 +18,6 @@ describe('Pipeline editor file nav', () => {
const fileIcon = () => wrapper.findComponent(FileIcon);
const link = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
index 555b9f29fbf..a651664851e 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -26,11 +26,6 @@ describe('Pipeline editor header', () => {
const findPipelineStatus = () => wrapper.findComponent(PipelineStatus);
const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('hides the pipeline status for new projects without a CI file', () => {
createComponent({ props: { isNewCiConfigFile: true } });
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index a62c51ffb59..3faa2890254 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -48,7 +48,6 @@ describe('Pipeline Status', () => {
afterEach(() => {
mockPipelineQuery.mockReset();
- wrapper.destroy();
});
describe('loading icon', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
index 0853a6f4ca4..a107a626c6d 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,11 +1,10 @@
import VueApollo from 'vue-apollo';
-import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import Vue from 'vue';
import { escape } from 'lodash';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import ValidationSegment, {
i18n,
} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
@@ -20,8 +19,8 @@ import {
} from '~/ci/pipeline_editor/constants';
import {
mergeUnwrappedCiConfig,
+ mockCiTroubleshootingPath,
mockCiYml,
- mockLintUnavailableHelpPagePath,
mockYmlHelpPagePath,
} from '../../mock_data';
@@ -43,29 +42,27 @@ describe('Validation segment component', () => {
},
});
- wrapper = extendedWrapper(
- shallowMount(ValidationSegment, {
- apolloProvider: mockApollo,
- provide: {
- ymlHelpPagePath: mockYmlHelpPagePath,
- lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
- },
- propsData: {
- ciConfig: mergeUnwrappedCiConfig(),
- ciFileContent: mockCiYml,
- ...props,
- },
- }),
- );
+ wrapper = shallowMountExtended(ValidationSegment, {
+ apolloProvider: mockApollo,
+ provide: {
+ ymlHelpPagePath: mockYmlHelpPagePath,
+ ciTroubleshootingPath: mockCiTroubleshootingPath,
+ },
+ propsData: {
+ ciConfig: mergeUnwrappedCiConfig(),
+ ciFileContent: mockCiYml,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
};
const findIcon = () => wrapper.findComponent(GlIcon);
- const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
- const findValidationMsg = () => wrapper.findByTestId('validationMsg');
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findValidationMsg = () => wrapper.findComponent(GlSprintf);
+ const findValidationSegment = () => wrapper.findByTestId('validation-segment');
it('shows the loading state', () => {
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
@@ -82,8 +79,12 @@ describe('Validation segment component', () => {
expect(findIcon().props('name')).toBe('check');
});
+ it('does not render a link', () => {
+ expect(findHelpLink().exists()).toBe(false);
+ });
+
it('shows a message for empty state', () => {
- expect(findValidationMsg().text()).toBe(i18n.empty);
+ expect(findValidationSegment().text()).toBe(i18n.empty);
});
});
@@ -97,12 +98,15 @@ describe('Validation segment component', () => {
});
it('shows a message for valid state', () => {
- expect(findValidationMsg().text()).toContain(i18n.valid);
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.valid, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
- expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
});
});
@@ -117,13 +121,16 @@ describe('Validation segment component', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
- it('has message for invalid state', () => {
- expect(findValidationMsg().text()).toBe(i18n.invalid);
+ it('shows a message for invalid state', () => {
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.invalid, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
- expect(findLearnMoreLink().text()).toBe('Learn more');
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
});
describe('with multiple errors', () => {
@@ -140,11 +147,16 @@ describe('Validation segment component', () => {
},
});
});
+
+ it('shows the learn more link', () => {
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
+ });
+
it('shows an invalid state with an error', () => {
- // Test the error is shown _and_ the string matches
- expect(findValidationMsg().text()).toContain(firstError);
- expect(findValidationMsg().text()).toBe(
- sprintf(i18n.invalidWithReason, { reason: firstError }),
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.invalidWithReason, { reason: firstError, linkStart: '', linkEnd: '' }),
);
});
});
@@ -163,10 +175,8 @@ describe('Validation segment component', () => {
});
});
it('shows an invalid state with an error while preventing XSS', () => {
- const { innerHTML } = findValidationMsg().element;
-
- expect(innerHTML).not.toContain(evilError);
- expect(innerHTML).toContain(escape(evilError));
+ expect(findValidationSegment().html()).not.toContain(evilError);
+ expect(findValidationSegment().html()).toContain(escape(evilError));
});
});
});
@@ -182,16 +192,18 @@ describe('Validation segment component', () => {
});
it('show a message that the service is unavailable', () => {
- expect(findValidationMsg().text()).toBe(i18n.unavailableValidation);
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.unavailableValidation, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the time-out icon', () => {
expect(findIcon().props('name')).toBe('time-out');
});
- it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath);
- expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ it('shows the link to ci troubleshooting', () => {
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(mockCiTroubleshootingPath);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js
new file mode 100644
index 00000000000..c7c40c3a4b9
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js
@@ -0,0 +1,39 @@
+import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Image item', () => {
+ let wrapper;
+
+ const findImageNameInput = () => wrapper.findByTestId('image-name-input');
+ const findImageEntrypointInput = () => wrapper.findByTestId('image-entrypoint-input');
+
+ const dummyImageName = 'dummyImageName';
+ const dummyImageEntrypoint = 'dummyImageEntrypoint';
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ImageItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findImageNameInput().vm.$emit('input', dummyImageName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['image.name', dummyImageName]);
+
+ findImageEntrypointInput().vm.$emit('input', dummyImageEntrypoint);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual(['image.entrypoint', [dummyImageEntrypoint]]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
new file mode 100644
index 00000000000..eaad0dae90d
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
@@ -0,0 +1,61 @@
+import createStore from '~/ci/pipeline_editor/store';
+import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Job setup item', () => {
+ let wrapper;
+
+ const findJobNameInput = () => wrapper.findByTestId('job-name-input');
+ const findJobScriptInput = () => wrapper.findByTestId('job-script-input');
+ const findJobTagsInput = () => wrapper.findByTestId('job-tags-input');
+ const findJobStageInput = () => wrapper.findByTestId('job-stage-input');
+
+ const dummyJobName = 'dummyJobName';
+ const dummyJobScript = 'dummyJobScript';
+ const dummyJobStage = 'dummyJobStage';
+ const dummyJobTags = ['tag1'];
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(JobSetupItem, {
+ store: createStore(),
+ propsData: {
+ tagOptions: [
+ { id: 'tag1', name: 'tag1' },
+ { id: 'tag2', name: 'tag2' },
+ ],
+ isNameValid: true,
+ isScriptValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findJobNameInput().vm.$emit('input', dummyJobName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['name', dummyJobName]);
+
+ findJobScriptInput().vm.$emit('input', dummyJobScript);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual(['script', dummyJobScript]);
+
+ findJobStageInput().vm.$emit('input', dummyJobStage);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual(['stage', dummyJobStage]);
+
+ findJobTagsInput().vm.$emit('input', dummyJobTags);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual(['tags', dummyJobTags]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
index 79200d92598..b293805d653 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -1,24 +1,47 @@
import { GlDrawer } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import { stringify } from 'yaml';
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
+import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
+import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
+import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createStore from '~/ci/pipeline_editor/store';
+import { mockAllRunnersQueryResponse } from 'jest/ci/pipeline_editor/mock_data';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
Vue.use(VueApollo);
describe('Job assistant drawer', () => {
let wrapper;
+ let mockApollo;
+
+ const dummyJobName = 'a';
+ const dummyJobScript = 'b';
+ const dummyImageName = 'c';
+ const dummyImageEntrypoint = 'd';
const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findJobSetupItem = () => wrapper.findComponent(JobSetupItem);
+ const findImageItem = () => wrapper.findComponent(ImageItem);
+ const findConfirmButton = () => wrapper.findByTestId('confirm-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const createComponent = () => {
+ mockApollo = createMockApollo([
+ [getAllRunners, jest.fn().mockResolvedValue(mockAllRunnersQueryResponse)],
+ ]);
+
wrapper = mountExtended(JobAssistantDrawer, {
+ store: createStore(),
propsData: {
isVisible: true,
},
+ apolloProvider: mockApollo,
});
};
@@ -27,6 +50,14 @@ describe('Job assistant drawer', () => {
await waitForPromises();
});
+ it('should contain job setup accordion', () => {
+ expect(findJobSetupItem().exists()).toBe(true);
+ });
+
+ it('should contain image accordion', () => {
+ expect(findImageItem().exists()).toBe(true);
+ });
+
it('should emit close job assistant drawer event when closing the drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
@@ -42,4 +73,83 @@ describe('Job assistant drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
});
+
+ it('trigger validate if job name is empty', async () => {
+ const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
+ findJobSetupItem().vm.$emit('update-job', 'script', 'b');
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('isNameValid')).toBe(false);
+ expect(findJobSetupItem().props('isScriptValid')).toBe(true);
+ expect(updateCiConfigSpy).toHaveBeenCalledTimes(0);
+ });
+
+ describe('when enter valid input', () => {
+ beforeEach(() => {
+ findJobSetupItem().vm.$emit('update-job', 'name', dummyJobName);
+ findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript);
+ findImageItem().vm.$emit('update-job', 'image.name', dummyImageName);
+ findImageItem().vm.$emit('update-job', 'image.entrypoint', [dummyImageEntrypoint]);
+ });
+
+ it('passes correct prop to accordions', () => {
+ const accordions = [findJobSetupItem(), findImageItem()];
+ accordions.forEach((accordion) => {
+ expect(accordion.props('job')).toMatchObject({
+ name: dummyJobName,
+ script: dummyJobScript,
+ image: {
+ name: dummyImageName,
+ entrypoint: [dummyImageEntrypoint],
+ },
+ });
+ });
+ });
+
+ it('job name and script state should be valid', () => {
+ expect(findJobSetupItem().props('isNameValid')).toBe(true);
+ expect(findJobSetupItem().props('isScriptValid')).toBe(true);
+ });
+
+ it('should clear job data when click confirm button', async () => {
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
+ });
+
+ it('should clear job data when click cancel button', async () => {
+ findCancelButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
+ });
+
+ it('should update correct ci content when click add button', () => {
+ const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
+
+ findConfirmButton().trigger('click');
+
+ expect(updateCiConfigSpy).toHaveBeenCalledWith(
+ `\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ },
+ })}`,
+ );
+ });
+
+ it('should emit scroll editor to button event when click add button', () => {
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+
+ findConfirmButton().trigger('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith(SCROLL_EDITOR_TO_BOTTOM);
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
index d43bdec3a33..cc9a77ae525 100644
--- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -40,10 +40,6 @@ describe('CI Lint Results', () => {
const findAfterScripts = findAllByTestId('after-script');
const filterEmptyScripts = (property) => mockJobs.filter((job) => job[property].length !== 0);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Empty results', () => {
it('renders with no jobs, errors or warnings defined', () => {
createComponent({ jobs: undefined, errors: undefined, warnings: undefined }, shallowMount);
diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index b5e3ea06c2c..d09e22898cd 100644
--- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -21,11 +21,6 @@ describe('CI lint warnings', () => {
const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]');
const findWarningMessage = () => trimText(wrapper.findComponent(GlSprintf).text());
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays the warning alert', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index f40db50aab7..52a543c7686 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -119,6 +119,7 @@ describe('Pipeline editor tabs component', () => {
});
afterEach(() => {
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
index 63ebfc0559d..a9aabb103f2 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -22,7 +22,6 @@ describe('FileTreePopover component', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('default', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
index cf0b974081e..23f9c7a87ee 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
@@ -19,10 +19,6 @@ describe('ValidatePopover component', () => {
const findHelpLink = () => wrapper.findByTestId('help-link');
const findFeedbackLink = () => wrapper.findByTestId('feedback-link');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(async () => {
createComponent({
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index ca6033f2ff5..186fd803d47 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -12,10 +12,6 @@ describe('WalkthroughPopover component', () => {
return extendedWrapper(mountFn(WalkthroughPopover));
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('CTA button clicked', () => {
beforeEach(async () => {
wrapper = createComponent(mount);
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
index b22c98e5544..8b8dd4d22c2 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -4,10 +4,9 @@ import ConfirmDialog from '~/ci/pipeline_editor/components/ui/confirm_unsaved_ch
describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
let beforeUnloadEvent;
let setDialogContent;
- let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(ConfirmDialog, {
+ shallowMount(ConfirmDialog, {
propsData,
});
};
@@ -21,7 +20,6 @@ describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
afterEach(() => {
beforeUnloadEvent.preventDefault.mockRestore();
setDialogContent.mockRestore();
- wrapper.destroy();
});
it('shows confirmation dialog when there are unsaved changes', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index 3c68f74af43..e636a89c6d9 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -23,10 +23,6 @@ describe('Pipeline editor empty state', () => {
const findConfirmButton = () => wrapper.findComponent(GlButton);
const findDescription = () => wrapper.findComponent(GlSprintf);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when project uses an external CI config', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
index ae25142b455..8874add6bb2 100644
--- a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
@@ -99,10 +99,6 @@ describe('Pipeline Editor Validate Tab', () => {
mockBlobContentData = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while initial CI content is loading', () => {
beforeEach(() => {
createComponent({ isBlobLoading: true });
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 541123d7efc..ecfc477184b 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -12,7 +12,7 @@ export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockIncludesHelpPagePath = '/-/includes/help';
export const mockLintHelpPagePath = '/-/lint-help';
-export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
+export const mockCiTroubleshootingPath = '/-/pipeline-editor/troubleshoot';
export const mockSimulatePipelineHelpPagePath = '/-/simulate-pipeline-help';
export const mockYmlHelpPagePath = '/-/yml-help';
export const mockCommitMessage = 'My commit message';
@@ -583,6 +583,91 @@ export const mockCommitCreateResponse = {
},
};
+export const mockAllRunnersQueryResponse = {
+ data: {
+ runners: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Runner/1',
+ description: 'test',
+ runnerType: 'PROJECT_TYPE',
+ shortSha: 'DdTYMQGS',
+ version: '15.6.1',
+ ipAddress: '127.0.0.1',
+ active: true,
+ locked: true,
+ jobCount: 0,
+ jobExecutionStatus: 'IDLE',
+ tagList: ['tag1', 'tag2', 'tag3'],
+ createdAt: '2022-11-29T09:37:43Z',
+ contactedAt: null,
+ status: 'NEVER_CONTACTED',
+ userPermissions: {
+ updateRunner: true,
+ deleteRunner: true,
+ __typename: 'RunnerPermissions',
+ },
+ groups: null,
+ ownerProject: {
+ id: 'gid://gitlab/Project/1',
+ name: '123',
+ nameWithNamespace: 'Administrator / 123',
+ webUrl: 'http://127.0.0.1:3000/root/test',
+ __typename: 'Project',
+ },
+ __typename: 'CiRunner',
+ upgradeStatus: 'NOT_AVAILABLE',
+ adminUrl: 'http://127.0.0.1:3000/admin/runners/1',
+ editAdminUrl: 'http://127.0.0.1:3000/admin/runners/1/edit',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/2',
+ description: 'test',
+ runnerType: 'PROJECT_TYPE',
+ shortSha: 'DdTYMQGA',
+ version: '15.6.1',
+ ipAddress: '127.0.0.1',
+ active: true,
+ locked: true,
+ jobCount: 0,
+ jobExecutionStatus: 'IDLE',
+ tagList: ['tag3', 'tag4'],
+ createdAt: '2022-11-29T09:37:43Z',
+ contactedAt: null,
+ status: 'NEVER_CONTACTED',
+ userPermissions: {
+ updateRunner: true,
+ deleteRunner: true,
+ __typename: 'RunnerPermissions',
+ },
+ groups: null,
+ ownerProject: {
+ id: 'gid://gitlab/Project/1',
+ name: '123',
+ nameWithNamespace: 'Administrator / 123',
+ webUrl: 'http://127.0.0.1:3000/root/test',
+ __typename: 'Project',
+ },
+ __typename: 'CiRunner',
+ upgradeStatus: 'NOT_AVAILABLE',
+ adminUrl: 'http://127.0.0.1:3000/admin/runners/2',
+ editAdminUrl: 'http://127.0.0.1:3000/admin/runners/2/edit',
+ },
+ ],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
+ __typename: 'PageInfo',
+ },
+ __typename: 'CiRunnerConnection',
+ },
+ },
+};
+
export const mockCommitCreateResponseNewEtag = {
data: {
commitCreate: {
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index a103acb33bc..7a13bfbd1ab 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
+import createStore from '~/ci/pipeline_editor/store';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
@@ -80,7 +81,9 @@ describe('Pipeline editor app component', () => {
provide = {},
stubs = {},
} = {}) => {
+ const store = createStore();
wrapper = shallowMount(PipelineEditorApp, {
+ store,
provide: { ...defaultProvide, ...provide },
stubs,
mocks: {
@@ -162,10 +165,6 @@ describe('Pipeline editor app component', () => {
mockPipelineQuery = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true });
@@ -256,6 +255,10 @@ describe('Pipeline editor app component', () => {
.mockImplementation(jest.fn());
});
+ it('available stages is updated', () => {
+ expect(wrapper.vm.$store.state.availableStages).toStrictEqual(['test', 'build']);
+ });
+
it('shows pipeline editor home component', () => {
expect(findEditorHome().exists()).toBe(true);
});
@@ -351,7 +354,9 @@ describe('Pipeline editor app component', () => {
});
it('shows that the lint service is down', () => {
- expect(findValidationSegment().text()).toContain(
+ const validationMessage = findValidationSegment().findComponent(GlSprintf);
+
+ expect(validationMessage.attributes('message')).toContain(
validationSegmenti18n.unavailableValidation,
);
});
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 4f8f2112abe..7ec6d4c6a01 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -67,7 +67,6 @@ describe('Pipeline editor home wrapper', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('renders', () => {
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index 6f18899ebac..1349461d8bc 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -32,6 +32,7 @@ import {
mockProjectId,
mockRefs,
mockYamlVariables,
+ mockPipelineConfigButtonText,
} from '../mock_data';
Vue.use(VueApollo);
@@ -42,6 +43,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines';
+const pipelinesEditorPath = '/root/project/-/ci/editor';
const projectPath = '/root/project/-/pipelines/config_variables';
const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'main';
@@ -65,6 +67,7 @@ describe('Pipeline New Form', () => {
wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert');
+ const findPipelineConfigButton = () => wrapper.findByTestId('ci-cd-pipeline-configuration');
const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert');
const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning');
@@ -106,6 +109,8 @@ describe('Pipeline New Form', () => {
propsData: {
projectId: mockProjectId,
pipelinesPath,
+ pipelinesEditorPath,
+ canViewPipelineEditor: true,
projectPath,
defaultBranch,
refParam: defaultBranch,
@@ -128,7 +133,6 @@ describe('Pipeline New Form', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('Form', () => {
@@ -500,6 +504,17 @@ describe('Pipeline New Form', () => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
+ it('shows pipeline configuration button for user who can view', () => {
+ expect(findPipelineConfigButton().exists()).toBe(true);
+ expect(findPipelineConfigButton().text()).toBe(mockPipelineConfigButtonText);
+ });
+
+ it('does not show pipeline configuration button for user who can not view', () => {
+ createComponentWithApollo({ props: { canViewPipelineEditor: false } });
+
+ expect(findPipelineConfigButton().exists()).toBe(false);
+ });
+
it('does not show the credit card validation required alert', () => {
expect(findCCAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
index cf8009e388f..60ace483712 100644
--- a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlListbox, GlListboxItem } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -13,13 +13,13 @@ const projectRefsEndpoint = '/root/project/refs';
const refShortName = 'main';
const refFullName = 'refs/heads/main';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Pipeline New Form', () => {
let wrapper;
let mock;
- const findDropdown = () => wrapper.findComponent(GlListbox);
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findRefsDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findSearchBox = () => wrapper.findByTestId('listbox-search-input');
const findListboxGroups = () => wrapper.findAll('ul[role="group"]');
diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index 5b935c0c819..175f513217b 100644
--- a/spec/frontend/ci/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -133,3 +133,5 @@ export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQue
mockYamlVariablesWithoutDesc,
);
export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null);
+
+export const mockPipelineConfigButtonText = 'Go to the pipeline editor';
diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
index ba948f12b33..c45267e5a47 100644
--- a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
@@ -20,10 +20,6 @@ describe('Delete pipeline schedule modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits the deleteSchedule event', async () => {
findModal().vm.$emit('primary');
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index 611993556e3..50008cedd9c 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -16,6 +16,7 @@ import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/g
import {
mockGetPipelineSchedulesGraphQLResponse,
mockPipelineScheduleNodes,
+ mockPipelineScheduleCurrentUser,
deleteMutationResponse,
playMutationResponse,
takeOwnershipMutationResponse,
@@ -79,10 +80,6 @@ describe('Pipeline schedules app', () => {
const findSchedulesCharacteristics = () =>
wrapper.findByTestId('pipeline-schedules-characteristics');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -115,6 +112,7 @@ describe('Pipeline schedules app', () => {
await waitForPromises();
expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
+ expect(findTable().props('currentUser')).toEqual(mockPipelineScheduleCurrentUser);
});
it('shows query error alert', async () => {
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index 6fb6a8bc33b..be0052fc7cf 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
import {
mockPipelineScheduleNodes,
+ mockPipelineScheduleCurrentUser,
mockPipelineScheduleAsGuestNodes,
mockTakeOwnershipNodes,
} from '../../../mock_data';
@@ -12,6 +13,7 @@ describe('Pipeline schedule actions', () => {
const defaultProps = {
schedule: mockPipelineScheduleNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
};
const createComponent = (props = defaultProps) => {
@@ -27,18 +29,17 @@ describe('Pipeline schedule actions', () => {
const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn');
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays action buttons', () => {
+ it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => {
createComponent();
expect(findAllButtons()).toHaveLength(3);
});
- it('does not display action buttons', () => {
- createComponent({ schedule: mockPipelineScheduleAsGuestNodes[0] });
+ it('does not display action buttons when user is not owner and does not have adminPipelineSchedule permission', () => {
+ createComponent({
+ schedule: mockPipelineScheduleAsGuestNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
+ });
expect(findAllButtons()).toHaveLength(0);
});
@@ -54,7 +55,10 @@ describe('Pipeline schedule actions', () => {
});
it('take ownership button emits showTakeOwnershipModal event and schedule id', () => {
- createComponent({ schedule: mockTakeOwnershipNodes[0] });
+ createComponent({
+ schedule: mockTakeOwnershipNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
+ });
findTakeOwnershipBtn().vm.$emit('click');
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 0821c59c8a0..ae069145292 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -21,10 +21,6 @@ describe('Pipeline schedule last pipeline', () => {
const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays pipeline status', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
index 1c06c411097..3bdbb371ddc 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
@@ -21,10 +21,6 @@ describe('Pipeline schedule next run', () => {
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
const findInactive = () => wrapper.findByTestId('pipeline-schedule-inactive');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays time ago', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
index 6c1991cb4ac..849bef80f42 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
@@ -25,10 +25,6 @@ describe('Pipeline schedule owner', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays avatar', () => {
expect(findAvatar().exists()).toBe(true);
expect(findAvatar().props('src')).toBe(defaultProps.schedule.owner.avatarUrl);
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
index f531f04a736..5cc3829efbd 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
@@ -25,10 +25,6 @@ describe('Pipeline schedule target', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays icon', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().props('name')).toBe('fork');
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
index 316b3bcf926..e488a36f3dc 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -1,13 +1,14 @@
import { GlTableLite } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
-import { mockPipelineScheduleNodes } from '../../mock_data';
+import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../../mock_data';
describe('Pipeline schedules table', () => {
let wrapper;
const defaultProps = {
schedules: mockPipelineScheduleNodes,
+ currentUser: mockPipelineScheduleCurrentUser,
};
const createComponent = (props = defaultProps) => {
@@ -25,10 +26,6 @@ describe('Pipeline schedules table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
index 7e6d4ec4bf8..e4ff9a0545b 100644
--- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
@@ -25,14 +25,12 @@ describe('Take ownership modal', () => {
const actionPrimary = findModal().props('actionPrimary');
expect(actionPrimary.attributes).toEqual(
- expect.objectContaining([
- {
- category: 'primary',
- variant: 'confirm',
- href: url,
- 'data-method': 'post',
- },
- ]),
+ expect.objectContaining({
+ category: 'primary',
+ variant: 'confirm',
+ href: url,
+ 'data-method': 'post',
+ }),
);
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 2826c054249..1485f6beea4 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -5,6 +5,7 @@ import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/
const {
data: {
+ currentUser,
project: {
pipelineSchedules: { nodes },
},
@@ -28,6 +29,7 @@ const {
} = mockGetPipelineSchedulesTakeOwnershipGraphQLResponse;
export const mockPipelineScheduleNodes = nodes;
+export const mockPipelineScheduleCurrentUser = currentUser;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index 90ca2a07266..f7386cfec74 100644
--- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -30,11 +30,6 @@ describe('code quality issue body issue body', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
index 3e4adfc7794..8beec220802 100644
--- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
@@ -15,10 +15,6 @@ describe('Grouped Issues List', () => {
const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a smart virtual list with the correct props', () => {
createComponent({
propsData: {
diff --git a/spec/frontend/ci/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
index fb13d4407e2..82b655dd598 100644
--- a/spec/frontend/ci/reports/components/issue_status_icon_spec.js
+++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
@@ -13,11 +13,6 @@ describe('IssueStatusIcon', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])(
'renders "%s" state correctly',
(status) => {
diff --git a/spec/frontend/ci/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js
index ba541ba0303..4a97afd77df 100644
--- a/spec/frontend/ci/reports/components/report_link_spec.js
+++ b/spec/frontend/ci/reports/components/report_link_spec.js
@@ -4,10 +4,6 @@ import ReportLink from '~/ci/reports/components/report_link.vue';
describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const defaultProps = {
issue: {},
};
diff --git a/spec/frontend/ci/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js
index f032b210184..f4012fe0215 100644
--- a/spec/frontend/ci/reports/components/report_section_spec.js
+++ b/spec/frontend/ci/reports/components/report_section_spec.js
@@ -49,10 +49,6 @@ describe('ReportSection component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('isCollapsible', () => {
const testMatrix = [
diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
index fb2ae5371d5..b1ae9e26b5b 100644
--- a/spec/frontend/ci/reports/components/summary_row_spec.js
+++ b/spec/frontend/ci/reports/components/summary_row_spec.js
@@ -31,11 +31,6 @@ describe('Summary row', () => {
const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders provided summary', () => {
createComponent();
expect(findSummary().text()).toContain(summary);
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
index edf3d1706cc..85b1d3b1b2f 100644
--- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -1,40 +1,53 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
-import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
-import { DEFAULT_PLATFORM } from '~/ci/runner/constants';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { runnerCreateResult } from '../mock_data';
const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
Vue.use(VueApollo);
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
describe('AdminNewRunnerApp', () => {
let wrapper;
const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
- wrapper = mountFn(AdminNewRunnerApp, {
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdminNewRunnerApp, {
propsData: {
legacyRegistrationToken: mockLegacyRegistrationToken,
- ...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlSprintf,
},
- ...options,
});
};
@@ -56,25 +69,59 @@ describe('AdminNewRunnerApp', () => {
});
});
- describe('New runner form fields', () => {
- describe('Platform', () => {
- it('shows the platforms radio group', () => {
- expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
- });
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner form', () => {
+ it('shows the runner create form', () => {
+ expect(findRunnerCreateForm().exists()).toBe(true);
});
- describe('Runner', () => {
- it('shows the runners fields', () => {
- expect(findRunnerFormFields().props('value')).toEqual({
- accessLevel: 'NOT_PROTECTED',
- paused: false,
- description: '',
- maintenanceNote: '',
- maximumTimeout: ' ',
- runUntagged: false,
- tagList: '',
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
});
});
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url);
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.registerAdminUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url);
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
});
});
});
diff --git a/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
new file mode 100644
index 00000000000..d04df85d58f
--- /dev/null
+++ b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
@@ -0,0 +1,122 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import AdminRegisterRunnerApp from '~/ci/runner/admin_register_runner/admin_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/admin/runners';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('AdminRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdminRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(async () => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(async () => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ await nextTick();
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index ed4f43c12d8..9d9142f2c68 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -72,7 +72,6 @@ describe('AdminRunnerShowApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
describe('When showing runner details', () => {
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 7fc240e520b..0cf6241c24f 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -9,7 +9,7 @@ import {
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -70,7 +70,7 @@ const mockRunnersCount = runnersCountData.data.runners.count;
const mockRunnersHandler = jest.fn();
const mockRunnersCountHandler = jest.fn();
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -143,7 +143,6 @@ describe('AdminRunnersApp', () => {
mockRunnersHandler.mockReset();
mockRunnersCountHandler.mockReset();
showToast.mockReset();
- wrapper.destroy();
});
it('shows the runner setup instructions', () => {
diff --git a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
index 82e262d1b73..8ac0c5a61f8 100644
--- a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
@@ -31,10 +31,6 @@ describe('RunnerActionsCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Edit Action', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
index 3097e43e583..03f1ace3897 100644
--- a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
@@ -16,7 +16,7 @@ describe('RunnerOwnerCell', () => {
const createComponent = ({ runner } = {}) => {
wrapper = shallowMount(RunnerOwnerCell, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
runner,
@@ -24,10 +24,6 @@ describe('RunnerOwnerCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When its an instance runner', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 1ff60ff1a9d..ec23d8415e8 100644
--- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -34,10 +34,6 @@ describe('RunnerStatusCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays online status', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 1711df42491..585a03c0811 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -45,10 +45,6 @@ describe('RunnerTypeCell', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays the runner name as id and short token', () => {
expect(wrapper.text()).toContain(
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
index f536e0dcbcf..7748890cf77 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
@@ -17,16 +17,12 @@ describe('RunnerSummaryField', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
...options,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows content in slot', () => {
createComponent({
slots: { default: 'content' },
diff --git a/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..09d032fd32d
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,204 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`registration utils for "linux" platform commandPrompt is correct 1`] = `"$"`;
+
+exports[`registration utils for "linux" platform installScript is correct for "386" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "amd64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "arm" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "arm64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "386",
+ "arm",
+ "arm64",
+]
+`;
+
+exports[`registration utils for "linux" platform registerCommand is correct 1`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+ " --registration-token REGISTRATION_TOKEN",
+ " --description 'RUNNER'",
+]
+`;
+
+exports[`registration utils for "linux" platform registerCommand is correct 2`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "linux" platform runCommand is correct 1`] = `"gitlab-runner run"`;
+
+exports[`registration utils for "osx" platform commandPrompt is correct 1`] = `"$"`;
+
+exports[`registration utils for "osx" platform installScript is correct for "amd64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# The rest of the commands execute as the user who will run the runner
+# Register the runner (steps below), then run
+cd ~
+gitlab-runner install
+gitlab-runner start"
+`;
+
+exports[`registration utils for "osx" platform installScript is correct for "arm64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# The rest of the commands execute as the user who will run the runner
+# Register the runner (steps below), then run
+cd ~
+gitlab-runner install
+gitlab-runner start"
+`;
+
+exports[`registration utils for "osx" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "arm64",
+]
+`;
+
+exports[`registration utils for "osx" platform registerCommand is correct 1`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+ " --registration-token REGISTRATION_TOKEN",
+ " --description 'RUNNER'",
+]
+`;
+
+exports[`registration utils for "osx" platform registerCommand is correct 2`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "osx" platform runCommand is correct 1`] = `"gitlab-runner run"`;
+
+exports[`registration utils for "windows" platform commandPrompt is correct 1`] = `">"`;
+
+exports[`registration utils for "windows" platform installScript is correct for "386" architecture 1`] = `
+"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator
+# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner
+New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory
+
+# Change to the folder
+cd 'C:\\\\GitLab-Runner'
+
+# Download binary
+Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe\\" -OutFile \\"gitlab-runner.exe\\"
+
+# Register the runner (steps below), then run
+.\\\\gitlab-runner.exe install
+.\\\\gitlab-runner.exe start"
+`;
+
+exports[`registration utils for "windows" platform installScript is correct for "amd64" architecture 1`] = `
+"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator
+# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner
+New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory
+
+# Change to the folder
+cd 'C:\\\\GitLab-Runner'
+
+# Download binary
+Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe\\" -OutFile \\"gitlab-runner.exe\\"
+
+# Register the runner (steps below), then run
+.\\\\gitlab-runner.exe install
+.\\\\gitlab-runner.exe start"
+`;
+
+exports[`registration utils for "windows" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "386",
+]
+`;
+
+exports[`registration utils for "windows" platform registerCommand is correct 1`] = `
+Array [
+ ".\\\\gitlab-runner.exe register",
+ " --url http://test.host",
+ " --registration-token REGISTRATION_TOKEN",
+ " --description 'RUNNER'",
+]
+`;
+
+exports[`registration utils for "windows" platform registerCommand is correct 2`] = `
+Array [
+ ".\\\\gitlab-runner.exe register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "windows" platform runCommand is correct 1`] = `".\\\\gitlab-runner.exe run"`;
diff --git a/spec/frontend/ci/runner/components/registration/cli_command_spec.js b/spec/frontend/ci/runner/components/registration/cli_command_spec.js
new file mode 100644
index 00000000000..78c2b94c3ea
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/cli_command_spec.js
@@ -0,0 +1,39 @@
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('CliCommand', () => {
+ let wrapper;
+
+ // use .textContent instead of .text() to capture whitespace that's visible in <pre>
+ const getPreTextContent = () => wrapper.find('pre').element.textContent;
+ const getClipboardText = () => wrapper.findComponent(ClipboardButton).props('text');
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(CliCommand, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ it('when rendering a command', () => {
+ createComponent({
+ prompt: '#',
+ command: 'echo hi',
+ });
+
+ expect(getPreTextContent()).toBe('# echo hi');
+ expect(getClipboardText()).toBe('echo hi');
+ });
+
+ it('when rendering a multi-line command', () => {
+ createComponent({
+ prompt: '#',
+ command: ['git', ' --version'],
+ });
+
+ expect(getPreTextContent()).toBe('# git --version');
+ expect(getClipboardText()).toBe('git --version');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js
new file mode 100644
index 00000000000..0b438455b5b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js
@@ -0,0 +1,108 @@
+import { nextTick } from 'vue';
+import { GlDrawer, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import {
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ INSTALL_HELP_URL,
+} from '~/ci/runner/constants';
+import { installScript, platformArchitectures } from '~/ci/runner/components/registration/utils';
+
+const MOCK_WRAPPER_HEIGHT = '99px';
+const LINUX_ARCHS = platformArchitectures({ platform: LINUX_PLATFORM });
+const MACOS_ARCHS = platformArchitectures({ platform: MACOS_PLATFORM });
+
+jest.mock('~/lib/utils/dom_utils', () => ({
+ getContentWrapperHeight: () => MOCK_WRAPPER_HEIGHT,
+}));
+
+describe('RegistrationInstructions', () => {
+ let wrapper;
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findEnvironmentOptions = () =>
+ wrapper.findByLabelText(s__('Runners|Environment')).findAll('option');
+ const findArchitectureOptions = () =>
+ wrapper.findByLabelText(s__('Runners|Architecture')).findAll('option');
+ const findCliCommand = () => wrapper.findComponent(CliCommand);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(PlatformsDrawer, {
+ propsData: {
+ open: true,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ it('shows drawer', () => {
+ createComponent();
+
+ expect(findDrawer().props()).toMatchObject({
+ open: true,
+ headerHeight: MOCK_WRAPPER_HEIGHT,
+ });
+ });
+
+ it('closes drawer', () => {
+ createComponent();
+ findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close')).toHaveLength(1);
+ });
+
+ it('shows selection options', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(findEnvironmentOptions().wrappers.map((w) => w.attributes('value'))).toEqual([
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ ]);
+
+ expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual(
+ LINUX_ARCHS,
+ );
+ });
+
+ it('shows script', () => {
+ createComponent();
+
+ expect(findCliCommand().props('command')).toBe(
+ installScript({ platform: LINUX_PLATFORM, architecture: LINUX_ARCHS[0] }),
+ );
+ });
+
+ it('shows selection options for another platform', async () => {
+ createComponent({ mountFn: mountExtended });
+
+ findEnvironmentOptions().at(1).setSelected(); // macos
+ await nextTick();
+
+ expect(wrapper.emitted('selectPlatform')).toEqual([[MACOS_PLATFORM]]);
+
+ expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual(
+ MACOS_ARCHS,
+ );
+
+ expect(findCliCommand().props('command')).toBe(
+ installScript({ platform: MACOS_PLATFORM, architecture: MACOS_ARCHS[0] }),
+ );
+ });
+
+ it('shows external link for more information', () => {
+ createComponent();
+
+ expect(findLink().attributes('href')).toBe(INSTALL_HELP_URL);
+ expect(findLink().findComponent(GlIcon).props('name')).toBe('external-link');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 0daaca9c4ff..9ed59b0a57d 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -116,10 +116,6 @@ describe('RegistrationDropdown', () => {
await openModal();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('opens the modal with contents', () => {
const modalText = findModalContent();
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
new file mode 100644
index 00000000000..eb4b659091d
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -0,0 +1,293 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import {
+ DEFAULT_PLATFORM,
+ EXECUTORS_HELP_URL,
+ SERVICE_COMMANDS_HELP_URL,
+ STATUS_NEVER_CONTACTED,
+ STATUS_ONLINE,
+ RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
+ I18N_REGISTRATION_SUCCESS,
+} from '~/ci/runner/constants';
+import { runnerForRegistration } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+const MOCK_TOKEN = 'MOCK_TOKEN';
+const mockDescription = runnerForRegistration.data.runner.description;
+
+const mockRunner = {
+ ...runnerForRegistration.data.runner,
+ ephemeralAuthenticationToken: MOCK_TOKEN,
+};
+const mockRunnerWithoutToken = {
+ ...runnerForRegistration.data.runner,
+ ephemeralAuthenticationToken: null,
+};
+
+const mockRunnerId = `${getIdFromGraphQLId(mockRunner.id)}`;
+
+describe('RegistrationInstructions', () => {
+ let wrapper;
+ let mockRunnerQuery;
+
+ const findHeading = () => wrapper.find('h1');
+ const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i));
+ const findByText = (text, container = wrapper) => container.findByText(text);
+
+ const waitForPolling = async () => {
+ jest.advanceTimersByTime(RUNNER_REGISTRATION_POLLING_INTERVAL_MS);
+ await waitForPromises();
+ };
+
+ const mockResolvedRunner = (runner = mockRunner) => {
+ mockRunnerQuery.mockResolvedValue({
+ data: {
+ runner,
+ },
+ });
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(RegistrationInstructions, {
+ apolloProvider: createMockApollo([[runnerForRegistrationQuery, mockRunnerQuery]]),
+ propsData: {
+ runnerId: mockRunnerId,
+ platform: DEFAULT_PLATFORM,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerQuery = jest.fn();
+ mockResolvedRunner();
+ });
+
+ beforeEach(() => {
+ window.gon.gitlab_url = TEST_HOST;
+ });
+
+ it('loads runner with id', async () => {
+ createComponent();
+
+ expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id });
+ });
+
+ describe('heading', () => {
+ it('when runner is loaded, shows heading', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findHeading().text()).toContain(mockRunner.description);
+ });
+
+ it('when runner is loaded, shows heading safely', async () => {
+ mockResolvedRunner({
+ ...mockRunner,
+ description: '<script>hacked();</script>',
+ });
+
+ createComponent();
+ await waitForPromises();
+
+ expect(findHeading().text()).toBe('Register "<script>hacked();</script>" runner');
+ expect(findHeading().element.innerHTML).toBe(
+ 'Register "&lt;script&gt;hacked();&lt;/script&gt;" runner',
+ );
+ });
+
+ it('when runner is loading, shows default heading', () => {
+ createComponent();
+
+ expect(findHeading().text()).toBe(s__('Runners|Register runner'));
+ });
+ });
+
+ it('renders legacy instructions', () => {
+ createComponent();
+
+ findByText('How do I install GitLab Runner?').vm.$emit('click');
+
+ expect(wrapper.emitted('toggleDrawer')).toHaveLength(1);
+ });
+
+ describe('step 1', () => {
+ it('renders step 1', async () => {
+ createComponent();
+ await waitForPromises();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(CliCommand).props()).toEqual({
+ command: [
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --registration-token ${MOCK_TOKEN}`,
+ ` --description '${mockDescription}'`,
+ ],
+ prompt: '$',
+ });
+ expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN);
+ });
+
+ it('renders step 1 in loading state', () => {
+ createComponent();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(GlSkeletonLoader).exists()).toBe(true);
+ expect(step1.find('code').exists()).toBe(false);
+ expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
+ });
+
+ it('render step 1 after token is not visible', async () => {
+ mockResolvedRunner(mockRunnerWithoutToken);
+
+ createComponent();
+ await waitForPromises();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --description '${mockDescription}'`,
+ ]);
+ expect(step1.find('[data-testid="runner-token"]').exists()).toBe(false);
+ expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
+ });
+
+ describe('polling for changes', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('fetches data', () => {
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it('polls', async () => {
+ await waitForPolling();
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+
+ await waitForPolling();
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(3);
+ });
+
+ it('when runner is online, stops polling', async () => {
+ mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ await waitForPolling();
+
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+ await waitForPolling();
+
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+ });
+
+ it('when token is no longer visible in the API, it is still visible in the UI', async () => {
+ mockResolvedRunner(mockRunnerWithoutToken);
+ await waitForPolling();
+
+ const step1 = findStepAt(0);
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --registration-token ${MOCK_TOKEN}`,
+ ` --description '${mockDescription}'`,
+ ]);
+ expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN);
+ });
+
+ it('when runner is not available (e.g. deleted), the UI does not update', async () => {
+ mockResolvedRunner(null);
+ await waitForPolling();
+
+ const step1 = findStepAt(0);
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --registration-token ${MOCK_TOKEN}`,
+ ` --description '${mockDescription}'`,
+ ]);
+ expect(step1.find('[data-testid="runner-token"]').text()).toBe(MOCK_TOKEN);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(MOCK_TOKEN);
+ });
+ });
+ });
+
+ it('renders step 2', () => {
+ createComponent();
+ const step2 = findStepAt(1);
+
+ expect(findByText('Not sure which one to select?', step2).attributes('href')).toBe(
+ EXECUTORS_HELP_URL,
+ );
+ });
+
+ it('renders step 3', () => {
+ createComponent();
+ const step3 = findStepAt(2);
+
+ expect(step3.findComponent(CliCommand).props()).toEqual({
+ command: 'gitlab-runner run',
+ prompt: '$',
+ });
+
+ expect(findByText('system or user service', step3).attributes('href')).toBe(
+ SERVICE_COMMANDS_HELP_URL,
+ );
+ });
+
+ describe('success state', () => {
+ describe('when the runner has not been registered', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPolling();
+
+ mockResolvedRunner({ ...mockRunner, status: STATUS_NEVER_CONTACTED });
+
+ await waitForPolling();
+ });
+
+ it('does not show success message', () => {
+ expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
+ });
+ });
+
+ describe('when the runner has been registered', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPolling();
+
+ mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ await waitForPolling();
+ });
+
+ it('shows success message', () => {
+ expect(wrapper.text()).toContain('🎉');
+ expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 783a4d9252a..ff69fd6d3d6 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -5,14 +5,14 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
@@ -43,7 +43,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
});
@@ -63,10 +63,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays reset button', () => {
expect(findDropdownItem().exists()).toBe(true);
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index d2a51c0d910..4f44e6e10b2 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -27,10 +27,6 @@ describe('RegistrationToken', () => {
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays value and copy button', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/registration/utils_spec.js b/spec/frontend/ci/runner/components/registration/utils_spec.js
new file mode 100644
index 00000000000..acf5993b15b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/utils_spec.js
@@ -0,0 +1,118 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import {
+ DEFAULT_PLATFORM,
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+
+import {
+ commandPrompt,
+ registerCommand,
+ runCommand,
+ installScript,
+ platformArchitectures,
+} from '~/ci/runner/components/registration/utils';
+
+const REGISTRATION_TOKEN = 'REGISTRATION_TOKEN';
+const DESCRIPTION = 'RUNNER';
+
+describe('registration utils', () => {
+ beforeEach(() => {
+ window.gon.gitlab_url = TEST_HOST;
+ });
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])(
+ 'for "%s" platform',
+ (platform) => {
+ it('commandPrompt is correct', () => {
+ expect(commandPrompt({ platform })).toMatchSnapshot();
+ });
+
+ it('registerCommand is correct', () => {
+ expect(
+ registerCommand({
+ platform,
+ registrationToken: REGISTRATION_TOKEN,
+ description: DESCRIPTION,
+ }),
+ ).toMatchSnapshot();
+
+ expect(registerCommand({ platform })).toMatchSnapshot();
+ });
+
+ it('runCommand is correct', () => {
+ expect(runCommand({ platform })).toMatchSnapshot();
+ });
+ },
+ );
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM])('for "%s" platform', (platform) => {
+ it.each`
+ description | parameter
+ ${'my runner'} | ${"'my runner'"}
+ ${"bob's runner"} | ${"'bob'\\''s runner'"}
+ `('registerCommand escapes description `$description`', ({ description, parameter }) => {
+ expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`);
+ });
+ });
+
+ describe.each([WINDOWS_PLATFORM])('for "%s" platform', (platform) => {
+ it.each`
+ description | parameter
+ ${'my runner'} | ${"'my runner'"}
+ ${"bob's runner"} | ${"'bob''s runner'"}
+ `('registerCommand escapes description `$description`', ({ description, parameter }) => {
+ expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`);
+ });
+ });
+
+ describe('for missing platform', () => {
+ it('commandPrompt uses the default', () => {
+ const expected = commandPrompt({ platform: DEFAULT_PLATFORM });
+
+ expect(commandPrompt({ platform: null })).toEqual(expected);
+ expect(commandPrompt({ platform: undefined })).toEqual(expected);
+ });
+
+ it('registerCommand uses the default', () => {
+ const expected = registerCommand({
+ platform: DEFAULT_PLATFORM,
+ registrationToken: REGISTRATION_TOKEN,
+ });
+
+ expect(registerCommand({ platform: null, registrationToken: REGISTRATION_TOKEN })).toEqual(
+ expected,
+ );
+ expect(
+ registerCommand({ platform: undefined, registrationToken: REGISTRATION_TOKEN }),
+ ).toEqual(expected);
+ });
+
+ it('runCommand uses the default', () => {
+ const expected = runCommand({ platform: DEFAULT_PLATFORM });
+
+ expect(runCommand({ platform: null })).toEqual(expected);
+ expect(runCommand({ platform: undefined })).toEqual(expected);
+ });
+ });
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])(
+ 'for "%s" platform',
+ (platform) => {
+ describe('platformArchitectures', () => {
+ it('returns correct list of architectures', () => {
+ expect(platformArchitectures({ platform })).toMatchSnapshot();
+ });
+ });
+
+ describe('installScript', () => {
+ const architectures = platformArchitectures({ platform });
+
+ it.each(architectures)('is correct for "%s" architecture', (architecture) => {
+ expect(installScript({ platform, architecture })).toMatchSnapshot();
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
index 5df2e04c340..a1fd9e4c1aa 100644
--- a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
@@ -33,10 +33,6 @@ describe('RunnerAssignedItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Shows an avatar', () => {
const avatar = findAvatar();
diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 0dc5a90fb83..f609c6be41a 100644
--- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
@@ -15,7 +15,7 @@ import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RunnerBulkDelete', () => {
let wrapper;
@@ -51,7 +51,7 @@ describe('RunnerBulkDelete', () => {
runners: mockRunners,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
GlSprintf,
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
new file mode 100644
index 00000000000..1123a026a4d
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -0,0 +1,170 @@
+import Vue from 'vue';
+import { GlForm } from '@gitlab/ui';
+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 RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { DEFAULT_ACCESS_LEVEL } from '~/ci/runner/constants';
+import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { runnerCreateResult } from '../mock_data';
+
+jest.mock('~/ci/runner/sentry_utils');
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+const defaultRunnerModel = {
+ description: '',
+ accessLevel: DEFAULT_ACCESS_LEVEL,
+ paused: false,
+ maintenanceNote: '',
+ maximumTimeout: '',
+ runUntagged: false,
+ tagList: '',
+};
+
+Vue.use(VueApollo);
+
+describe('RunnerCreateForm', () => {
+ let wrapper;
+ let runnerCreateHandler;
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+ const findSubmitBtn = () => wrapper.find('[type="submit"]');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RunnerCreateForm, {
+ apolloProvider: createMockApollo([[runnerCreateMutation, runnerCreateHandler]]),
+ });
+ };
+
+ beforeEach(() => {
+ runnerCreateHandler = jest.fn().mockResolvedValue(runnerCreateResult);
+
+ createComponent();
+ });
+
+ it('shows default runner values', () => {
+ expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel);
+ });
+
+ it('shows a submit button', () => {
+ expect(findSubmitBtn().exists()).toBe(true);
+ });
+
+ describe('when user submits', () => {
+ let preventDefault;
+
+ beforeEach(() => {
+ preventDefault = jest.fn();
+
+ findRunnerFormFields().vm.$emit('input', {
+ ...defaultRunnerModel,
+ description: 'My runner',
+ maximumTimeout: 0,
+ tagList: 'tag1, tag2',
+ });
+ });
+
+ describe('immediately after submit', () => {
+ beforeEach(() => {
+ findForm().vm.$emit('submit', { preventDefault });
+ });
+
+ it('prevents default form submission', () => {
+ expect(preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(true);
+ });
+
+ it('saves runner', async () => {
+ expect(runnerCreateHandler).toHaveBeenCalledWith({
+ input: {
+ ...defaultRunnerModel,
+ description: 'My runner',
+ maximumTimeout: 0,
+ tagList: ['tag1', 'tag2'],
+ },
+ });
+ });
+ });
+
+ describe('when saved successfully', () => {
+ beforeEach(async () => {
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "saved" result', async () => {
+ expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when a server error occurs', () => {
+ const error = new Error('Error!');
+
+ beforeEach(async () => {
+ runnerCreateHandler.mockRejectedValue(error);
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" result', async () => {
+ expect(wrapper.emitted('error')[0]).toEqual([error]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('reports error', () => {
+ expect(captureException).toHaveBeenCalledTimes(1);
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerCreateForm',
+ error,
+ });
+ });
+ });
+
+ describe('when a validation error occurs', () => {
+ const errorMsg1 = 'Issue1!';
+ const errorMsg2 = 'Issue2!';
+
+ beforeEach(async () => {
+ runnerCreateHandler.mockResolvedValue({
+ data: {
+ runnerCreate: {
+ errors: [errorMsg1, errorMsg2],
+ runner: null,
+ },
+ },
+ });
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" results', async () => {
+ expect(wrapper.emitted('error')[0]).toEqual([new Error(`${errorMsg1} ${errorMsg2}`)]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('does not report error', () => {
+ expect(captureException).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index 02960ad427e..f9bea318d84 100644
--- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -8,7 +8,7 @@ import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutat
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
@@ -21,7 +21,7 @@ const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
@@ -53,8 +53,8 @@ describe('RunnerDeleteButton', () => {
},
apolloProvider,
directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlModal: createMockDirective('gl-modal'),
},
});
};
@@ -83,10 +83,6 @@ describe('RunnerDeleteButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays a delete button without an icon', () => {
expect(findBtn().props()).toMatchObject({
loading: false,
diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index 65a81973869..c2d9e86aa91 100644
--- a/spec/frontend/ci/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -37,10 +37,6 @@ describe('RunnerDetails', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Details tab', () => {
describe.each`
field | runner | expectedValue
diff --git a/spec/frontend/ci/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
index 907cdc90100..5cc1ee049f4 100644
--- a/spec/frontend/ci/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -11,7 +11,7 @@ describe('RunnerEditButton', () => {
wrapper = mountFn(RunnerEditButton, {
attrs,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -20,10 +20,6 @@ describe('RunnerEditButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays Edit text', () => {
expect(wrapper.attributes('aria-label')).toBe('Edit');
});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index 408750e646f..ac84c7898bf 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -65,10 +65,6 @@ describe('RunnerList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('binds a namespace to the filtered search', () => {
expect(findFilteredSearch().props('namespace')).toBe('runners');
});
diff --git a/spec/frontend/ci/runner/components/runner_groups_spec.js b/spec/frontend/ci/runner/components/runner_groups_spec.js
index 0991feb2e55..e4f5f55ab4b 100644
--- a/spec/frontend/ci/runner/components/runner_groups_spec.js
+++ b/spec/frontend/ci/runner/components/runner_groups_spec.js
@@ -23,10 +23,6 @@ describe('RunnerGroups', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Shows a heading', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index abe3b47767e..c851966431d 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -42,10 +42,6 @@ describe('RunnerHeader', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the runner status', () => {
createComponent({
mountFn: mountExtended,
diff --git a/spec/frontend/ci/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js
index bdb8a4a31a3..365b0f1f5ba 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
@@ -15,7 +15,7 @@ import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql'
import { runnerData, runnerJobsData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -47,7 +47,6 @@ describe('RunnerJobs', () => {
afterEach(() => {
mockRunnerJobsQuery.mockReset();
- wrapper.destroy();
});
it('Requests runner jobs', async () => {
diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 281aa1aeb77..694c5a6ed17 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -37,10 +37,6 @@ describe('RunnerJobsTable', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Sets job id as a row key', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 6aea3ddf58c..3e813723b5b 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -31,7 +31,7 @@ describe('RunnerListEmptyState', () => {
...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlEmptyState,
@@ -62,11 +62,11 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- describe('when create_runner_workflow is enabled', () => {
+ describe('when create_runner_workflow_for_admin is enabled', () => {
beforeEach(() => {
createComponent({
provide: {
- glFeatures: { createRunnerWorkflow: true },
+ glFeatures: { createRunnerWorkflowForAdmin: true },
},
});
});
@@ -76,14 +76,14 @@ describe('RunnerListEmptyState', () => {
});
});
- describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => {
+ describe('when create_runner_workflow_for_admin is enabled and newRunnerPath not defined', () => {
beforeEach(() => {
createComponent({
props: {
newRunnerPath: null,
},
provide: {
- glFeatures: { createRunnerWorkflow: true },
+ glFeatures: { createRunnerWorkflowForAdmin: true },
},
});
});
@@ -95,11 +95,11 @@ describe('RunnerListEmptyState', () => {
});
});
- describe('when create_runner_workflow is disabled', () => {
+ describe('when create_runner_workflow_for_admin is disabled', () => {
beforeEach(() => {
createComponent({
provide: {
- glFeatures: { createRunnerWorkflow: false },
+ glFeatures: { createRunnerWorkflowForAdmin: false },
},
});
});
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 2e5d1dbd063..6f4913dca3e 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -57,10 +57,6 @@ describe('RunnerList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays headers', () => {
createComponent(
{
diff --git a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
index f089becd400..7ff3ec92042 100644
--- a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
+++ b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
@@ -18,10 +18,6 @@ describe('RunnerMembershipToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays text', () => {
createComponent({ mountFn: mount });
diff --git a/spec/frontend/ci/runner/components/runner_pagination_spec.js b/spec/frontend/ci/runner/components/runner_pagination_spec.js
index f835ee4514d..6d84eb810f8 100644
--- a/spec/frontend/ci/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pagination_spec.js
@@ -16,10 +16,6 @@ describe('RunnerPagination', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When in between pages', () => {
const mockPageInfo = {
startCursor: mockStartCursor,
diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 12680e01b98..62e6cc902b7 100644
--- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -7,7 +7,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
@@ -22,7 +22,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0];
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerPauseButton', () => {
@@ -46,7 +46,7 @@ describe('RunnerPauseButton', () => {
},
apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]),
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -74,10 +74,6 @@ describe('RunnerPauseButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Pause/Resume action', () => {
describe.each`
runnerState | icon | content | tooltip | isActive | newActiveValue
diff --git a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
index b051ebe99a7..54768ea50da 100644
--- a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
@@ -16,7 +16,7 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -25,10 +25,6 @@ describe('RunnerTypeBadge', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders paused state', () => {
expect(wrapper.text()).toBe(I18N_PAUSED);
expect(findBadge().props('variant')).toBe('warning');
diff --git a/spec/frontend/ci/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js
index 17517c4db66..ccc1bc18675 100644
--- a/spec/frontend/ci/runner/components/runner_projects_spec.js
+++ b/spec/frontend/ci/runner/components/runner_projects_spec.js
@@ -4,7 +4,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { sprintf } from '~/locale';
import {
I18N_ASSIGNED_PROJECTS,
@@ -22,7 +22,7 @@ import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query.
import { runnerData, runnerProjectsData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -56,7 +56,6 @@ describe('RunnerProjects', () => {
afterEach(() => {
mockRunnerProjectsQuery.mockReset();
- wrapper.destroy();
});
it('Requests runner projects', async () => {
diff --git a/spec/frontend/ci/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 45b410df2d4..e1eb81f2d23 100644
--- a/spec/frontend/ci/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -31,7 +31,7 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -43,8 +43,6 @@ describe('RunnerTypeBadge', () => {
afterEach(() => {
jest.useFakeTimers({ legacyFakeTimers: true });
-
- wrapper.destroy();
});
it('renders online state', () => {
diff --git a/spec/frontend/ci/runner/components/runner_tag_spec.js b/spec/frontend/ci/runner/components/runner_tag_spec.js
index 7bcb046ae43..e3d46e5d6df 100644
--- a/spec/frontend/ci/runner/components/runner_tag_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tag_spec.js
@@ -29,8 +29,8 @@ describe('RunnerTag', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
};
@@ -39,10 +39,6 @@ describe('RunnerTag', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays tag text', () => {
expect(wrapper.text()).toBe(mockTag);
});
diff --git a/spec/frontend/ci/runner/components/runner_tags_spec.js b/spec/frontend/ci/runner/components/runner_tags_spec.js
index 96bec00302b..bcb1d1f9e13 100644
--- a/spec/frontend/ci/runner/components/runner_tags_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tags_spec.js
@@ -21,10 +21,6 @@ describe('RunnerTags', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays tags text', () => {
expect(wrapper.text()).toMatchInterpolatedText('tag1 tag2');
diff --git a/spec/frontend/ci/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index 58f09362759..7a0fb6f69ea 100644
--- a/spec/frontend/ci/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -23,15 +23,11 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
type | text
${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE}
diff --git a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
index 3347c190083..6e15c84ad7e 100644
--- a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
@@ -63,10 +63,6 @@ describe('RunnerTypeTabs', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Renders all options to filter runners by default', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index a0e51ebf958..620e2f85890 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -5,7 +5,7 @@ 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';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import {
@@ -21,7 +21,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerFormData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -107,10 +107,6 @@ describe('RunnerUpdateForm', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Form has a submit button', () => {
expect(findSubmit().exists()).toBe(true);
});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index b7d9d3ad23e..e9f2e888b9a 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -3,14 +3,14 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'),
@@ -90,7 +90,6 @@ describe('TagToken', () => {
afterEach(() => {
getRecentlyUsedSuggestions.mockReset();
- wrapper.destroy();
});
describe('when the tags token is displayed', () => {
diff --git a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
index cad61f26012..f30b75ee614 100644
--- a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
@@ -32,10 +32,6 @@ describe('RunnerStats', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
case | count | value
${'number'} | ${99} | ${'99'}
diff --git a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index 3d45674d106..13366a788d5 100644
--- a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -47,10 +47,6 @@ describe('RunnerStats', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays all the stats', () => {
createComponent({
mountFn: mount,
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index 2ad31dea774..fadc6e5ebc5 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -74,7 +74,6 @@ describe('GroupRunnerShowApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
describe('When showing runner details', () => {
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 39ea5cade28..00c7262e38b 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -9,7 +9,7 @@ import {
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -74,7 +74,7 @@ const mockGroupRunnersCount = mockGroupRunnersEdges.length;
const mockGroupRunnersHandler = jest.fn();
const mockGroupRunnersCountHandler = jest.fn();
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -138,7 +138,6 @@ describe('GroupRunnersApp', () => {
afterEach(() => {
mockGroupRunnersHandler.mockReset();
mockGroupRunnersCountHandler.mockReset();
- wrapper.destroy();
});
it('shows the runner tabs with a runner count for each type', async () => {
diff --git a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
index 03908891cfd..30e49fc7644 100644
--- a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
@@ -2,9 +2,9 @@ import AccessorUtilities from '~/lib/utils/accessor';
import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage';
import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('showAlertFromLocalStorage', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 5cdf0ea4e3b..092a419c1fe 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -1,6 +1,10 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
+// Register runner queries
+import runnerForRegistration from 'test_fixtures/graphql/ci/runner/register/runner_for_registration.query.graphql.json';
+
// Show runner queries
+import runnerCreateResult from 'test_fixtures/graphql/ci/runner/new/runner_create.mutation.graphql.json';
import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.json';
import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json';
import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json';
@@ -9,6 +13,8 @@ import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.que
// Edit runner queries
import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
+// New runner queries
+
// List queries
import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
@@ -321,4 +327,6 @@ export {
runnerProjectsData,
runnerJobsData,
runnerFormData,
+ runnerCreateResult,
+ runnerForRegistration,
};
diff --git a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index a9369a5e626..79bbf95f8f0 100644
--- a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -15,7 +15,7 @@ import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/ci/runner/con
import { runnerFormData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerFormData.data.runner;
@@ -51,7 +51,6 @@ describe('RunnerEditApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
it('expect GraphQL ID to be requested', async () => {
diff --git a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
index b2084e3a7de..1be89ae832d 100644
--- a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
+++ b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
@@ -15,7 +15,7 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
>
<table
- aria-busy="false"
+ aria-busy=""
aria-colcount="2"
class="table b-table gl-table"
role="table"
@@ -196,7 +196,7 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
>
<table
- aria-busy="false"
+ aria-busy=""
aria-colcount="2"
class="table b-table gl-table"
role="table"
diff --git a/spec/frontend/ci_secure_files/components/metadata/button_spec.js b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
index 4ac5b3325d4..5bd4bab25af 100644
--- a/spec/frontend/ci_secure_files/components/metadata/button_spec.js
+++ b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
@@ -12,10 +12,6 @@ describe('Secure File Metadata Button', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
const createWrapper = (secureFile = {}, admin = false) => {
wrapper = mount(Button, {
propsData: {
diff --git a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
index 230507d32d7..e181d15f2f9 100644
--- a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
+++ b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
@@ -37,7 +37,6 @@ describe('Secure File Metadata Modal', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
describe('when a .cer file is supplied', () => {
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 ab6200ca6f4..17b5fdc4dde 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
@@ -15,11 +15,6 @@ const dummyApiVersion = 'v3000';
const dummyProjectId = 1;
const fileSizeLimit = 5;
const dummyUrlRoot = '/gitlab';
-const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
-};
-let originalGon;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${dummyProjectId}/secure_files`;
describe('SecureFilesList', () => {
@@ -28,16 +23,16 @@ describe('SecureFilesList', () => {
let trackingSpy;
beforeEach(() => {
- originalGon = window.gon;
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
unmockTracking();
- window.gon = originalGon;
});
const createWrapper = (admin = true, props = {}) => {
diff --git a/spec/frontend/clusters/agents/components/activity_events_list_spec.js b/spec/frontend/clusters/agents/components/activity_events_list_spec.js
index 6b374b6620d..770815a9403 100644
--- a/spec/frontend/clusters/agents/components/activity_events_list_spec.js
+++ b/spec/frontend/clusters/agents/components/activity_events_list_spec.js
@@ -44,10 +44,6 @@ describe('ActivityEvents', () => {
const findAllActivityHistoryItems = () => wrapper.findAllComponents(ActivityHistoryItem);
const findSectionTitle = (at) => wrapper.findAllByTestId('activity-section-title').at(at);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while the agentEvents query is loading', () => {
it('displays a loading icon', async () => {
createWrapper();
diff --git a/spec/frontend/clusters/agents/components/activity_history_item_spec.js b/spec/frontend/clusters/agents/components/activity_history_item_spec.js
index 68f6f11aa8f..48460519c6c 100644
--- a/spec/frontend/clusters/agents/components/activity_history_item_spec.js
+++ b/spec/frontend/clusters/agents/components/activity_history_item_spec.js
@@ -25,10 +25,6 @@ describe('ActivityHistoryItem', () => {
const findHistoryItem = () => wrapper.findComponent(HistoryItem);
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
kind | icon | title | lineNumber
${'token_created'} | ${EVENT_DETAILS.token_created.eventTypeIcon} | ${sprintf(EVENT_DETAILS.token_created.title, { tokenName: agentName })} | ${0}
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
index db1219ccb41..ac0ce89f334 100644
--- a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
+++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
@@ -25,10 +25,6 @@ describe('IntegrationStatus', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findBadge = () => wrapper.findComponent(GlBadge);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('icon', () => {
const icon = 'status-success';
const iconClass = 'gl-text-green-500';
diff --git a/spec/frontend/clusters/agents/components/create_token_button_spec.js b/spec/frontend/clusters/agents/components/create_token_button_spec.js
index 73856b74a8d..5a8906813cf 100644
--- a/spec/frontend/clusters/agents/components/create_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_button_spec.js
@@ -21,7 +21,7 @@ describe('CreateTokenButton', () => {
...provideData,
},
directives: {
- GlModalDirective: createMockDirective(),
+ GlModalDirective: createMockDirective('gl-modal-directive'),
},
stubs: {
GlTooltip,
@@ -29,10 +29,6 @@ describe('CreateTokenButton', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user can create token', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
index 0d10801e80e..ff698952c6b 100644
--- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
@@ -119,7 +119,6 @@ describe('CreateTokenModal', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
createResponse = null;
});
diff --git a/spec/frontend/clusters/agents/components/integration_status_spec.js b/spec/frontend/clusters/agents/components/integration_status_spec.js
index 36f0e622452..28a59391578 100644
--- a/spec/frontend/clusters/agents/components/integration_status_spec.js
+++ b/spec/frontend/clusters/agents/components/integration_status_spec.js
@@ -27,10 +27,6 @@ describe('IntegrationStatus', () => {
const findAgentStatus = () => wrapper.findByTestId('agent-status');
const findAgentIntegrationStatusRows = () => wrapper.findAllComponents(AgentIntegrationStatusRow);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
lastUsedAt | status | iconName
${null} | ${'Never connected'} | ${'status-neutral'}
diff --git a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
index 6521221cbd7..ed7c940bb04 100644
--- a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
@@ -45,7 +45,7 @@ describe('RevokeTokenButton', () => {
const findInput = () => wrapper.findComponent(GlFormInput);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findPrimaryAction = () => findModal().props('actionPrimary');
- const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr];
const createMockApolloProvider = ({ mutationResponse }) => {
revokeSpy = jest.fn().mockResolvedValue(mutationResponse);
@@ -105,7 +105,6 @@ describe('RevokeTokenButton', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
revokeSpy = null;
});
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index efa85136b17..118a3af48e0 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -79,10 +79,6 @@ describe('ClusterAgentShow', () => {
const findActivity = () => wrapper.findComponent(ActivityEvents);
const findIntegrationStatus = () => wrapper.findComponent(IntegrationStatus);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default behaviour', () => {
beforeEach(async () => {
createWrapper({ clusterAgent: defaultClusterAgent });
diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js
index 334615f1818..1a6aeedb694 100644
--- a/spec/frontend/clusters/agents/components/token_table_spec.js
+++ b/spec/frontend/clusters/agents/components/token_table_spec.js
@@ -57,10 +57,6 @@ describe('ClusterAgentTokenTable', () => {
return createComponent(defaultTokens);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the create token button', () => {
expect(findCreateTokenBtn().exists()).toBe(true);
});
diff --git a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
index 656e72baf77..21ffda8578a 100644
--- a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
@@ -3,7 +3,7 @@
exports[`NewCluster renders the cluster component correctly 1`] = `
"<div class=\\"gl-pt-4\\">
<h4>Enter your Kubernetes cluster certificate details</h4>
- <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub>
+ <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub>
</p>
</div>"
`;
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index 46ee123a12d..67b0ecdf7eb 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -43,3 +43,212 @@ exports[`Remove cluster confirmation modal renders buttons with modal included 1
<!---->
</div>
`;
+
+exports[`Remove cluster confirmation modal two buttons open modal with "cleanup" option 1`] = `
+<div
+ class="gl-display-flex"
+>
+ <button
+ class="btn gl-mr-3 btn-danger btn-md gl-button"
+ data-testid="remove-integration-and-resources-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration and resources
+
+ </span>
+ </button>
+
+ <button
+ class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ data-testid="remove-integration-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration
+
+ </span>
+ </button>
+
+ <div
+ kind="danger"
+ >
+ <p>
+ You are about to remove your cluster integration and all GitLab-created resources associated with this cluster.
+ </p>
+
+ <div>
+
+ This will permanently delete the following resources:
+
+ <ul>
+ <li>
+ Any project namespaces
+ </li>
+
+ <li>
+ <code>
+ clusterroles
+ </code>
+ </li>
+
+ <li>
+ <code>
+ clusterrolebindings
+ </code>
+ </li>
+ </ul>
+ </div>
+
+ <strong>
+ To remove your integration and resources, type
+ <code>
+ my-test-cluster
+ </code>
+ to confirm:
+ </strong>
+
+ <form
+ action="clusterPath"
+ class="gl-mb-5"
+ method="post"
+ >
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ />
+
+ <input
+ name="cleanup"
+ type="hidden"
+ value="true"
+ />
+
+ <input
+ autocomplete="off"
+ class="gl-form-input form-control"
+ id="__BVID__14"
+ name="confirm_cluster_name_input"
+ type="text"
+ />
+ </form>
+
+ <span>
+ If you do not wish to delete all associated GitLab resources, you can simply remove the integration.
+ </span>
+ </div>
+</div>
+`;
+
+exports[`Remove cluster confirmation modal two buttons open modal without "cleanup" option 1`] = `
+<div
+ class="gl-display-flex"
+>
+ <button
+ class="btn gl-mr-3 btn-danger btn-md gl-button"
+ data-testid="remove-integration-and-resources-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration and resources
+
+ </span>
+ </button>
+
+ <button
+ class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ data-testid="remove-integration-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration
+
+ </span>
+ </button>
+
+ <div
+ kind="danger"
+ >
+ <p>
+ You are about to remove your cluster integration.
+ </p>
+
+ <!---->
+
+ <strong>
+ To remove your integration, type
+ <code>
+ my-test-cluster
+ </code>
+ to confirm:
+ </strong>
+
+ <form
+ action="clusterPath"
+ class="gl-mb-5"
+ method="post"
+ >
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ />
+
+ <input
+ name="cleanup"
+ type="hidden"
+ value="true"
+ />
+
+ <input
+ autocomplete="off"
+ class="gl-form-input form-control"
+ id="__BVID__21"
+ name="confirm_cluster_name_input"
+ type="text"
+ />
+ </form>
+
+ <!---->
+ </div>
+</div>
+`;
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
index ef39c90aaef..398b472a3a7 100644
--- a/spec/frontend/clusters/components/new_cluster_spec.js
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -20,10 +20,6 @@ describe('NewCluster', () => {
return createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the cluster component correctly', () => {
expect(wrapper.html()).toMatchSnapshot();
});
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index 53683af893a..04b7909b534 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -6,6 +6,7 @@ import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_conf
describe('Remove cluster confirmation modal', () => {
let wrapper;
+ const showMock = jest.fn();
const createComponent = ({ props = {}, stubs = {} } = {}) => {
wrapper = mount(RemoveClusterConfirmation, {
@@ -18,11 +19,6 @@ describe('Remove cluster confirmation modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders buttons with modal included', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
@@ -38,9 +34,13 @@ describe('Remove cluster confirmation modal', () => {
beforeEach(() => {
createComponent({
props: { clusterName: 'my-test-cluster' },
- stubs: { GlSprintf, GlModal: stubComponent(GlModal) },
+ stubs: {
+ GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ methods: { show: showMock },
+ }),
+ },
});
- jest.spyOn(findModal().vm, 'show').mockReturnValue();
});
it('open modal with "cleanup" option', async () => {
@@ -48,8 +48,8 @@ describe('Remove cluster confirmation modal', () => {
await nextTick();
- expect(findModal().vm.show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(true);
+ expect(showMock).toHaveBeenCalled();
+ expect(wrapper.element).toMatchSnapshot();
expect(findModal().html()).toContain(
'<strong>To remove your integration and resources, type <code>my-test-cluster</code> to confirm:</strong>',
);
@@ -60,8 +60,8 @@ describe('Remove cluster confirmation modal', () => {
await nextTick();
- expect(findModal().vm.show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(false);
+ expect(showMock).toHaveBeenCalled();
+ expect(wrapper.element).toMatchSnapshot();
expect(findModal().html()).toContain(
'<strong>To remove your integration, type <code>my-test-cluster</code> to confirm:</strong>',
);
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index b17886a5826..396f8215b9f 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -1,6 +1,6 @@
import { GlToggle, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
@@ -27,17 +27,9 @@ describe('ClusterIntegrationForm', () => {
});
};
- const destroyWrapper = () => {
- wrapper.destroy();
- wrapper = null;
- };
-
const findSubmitButton = () => wrapper.findComponent(GlButton);
const findGlToggle = () => wrapper.findComponent(GlToggle);
-
- afterEach(() => {
- destroyWrapper();
- });
+ const findClusterEnvironmentScopeInput = () => wrapper.find('[id="cluster_environment_scope"]');
describe('rendering', () => {
beforeEach(() => createWrapper());
@@ -50,7 +42,9 @@ describe('ClusterIntegrationForm', () => {
});
it('sets the envScope to default', () => {
- expect(wrapper.find('[id="cluster_environment_scope"]').attributes('value')).toBe('*');
+ expect(findClusterEnvironmentScopeInput().attributes('value')).toBe(
+ defaultStoreValues.environmentScope,
+ );
});
it('sets the baseDomain to default', () => {
@@ -76,20 +70,15 @@ describe('ClusterIntegrationForm', () => {
beforeEach(() => createWrapper());
it('enables the submit button on changing toggle to different value', async () => {
- await nextTick();
- // setData is a bad approach because it changes the internal implementation which we should not touch
- // but our GlFormInput lacks the ability to set a new value.
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
+ await findGlToggle().vm.$emit('change', false);
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('enables the submit button on changing input values', async () => {
- await nextTick();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
+ await findClusterEnvironmentScopeInput().vm.$emit(
+ 'input',
+ `${defaultStoreValues.environmentScope}1`,
+ );
expect(findSubmitButton().props('disabled')).toBe(false);
});
});
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index a92a03fedb6..edb8b22d79e 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -53,10 +53,6 @@ describe('InstallAgentModal', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('initial state', () => {
it('shows basic agent installation instructions', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallTitle);
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index 2372ab30300..d91245ba9b4 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -83,8 +83,6 @@ describe('Agents', () => {
const findBanner = () => wrapper.findComponent(GlBanner);
afterEach(() => {
- wrapper.destroy();
-
localStorage.removeItem(AGENT_FEEDBACK_KEY);
});
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index 758f6586e1a..4a2effa3463 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -18,10 +18,6 @@ describe('ClustersAncestorNotice', () => {
return createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when cluster does not have ancestors', () => {
beforeEach(async () => {
store.state.hasAncestorClusters = false;
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index f4ee3f93cb5..e4e1986f705 100644
--- a/spec/frontend/clusters_list/components/clusters_actions_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -35,7 +35,7 @@ describe('ClustersActionsComponent', () => {
...provideData,
},
directives: {
- GlModalDirective: createMockDirective(),
+ GlModalDirective: createMockDirective('gl-modal-directive'),
},
});
};
@@ -44,9 +44,6 @@ describe('ClustersActionsComponent', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
describe('when the certificate based clusters are enabled', () => {
it('renders actions menu', () => {
expect(findDropdown().exists()).toBe(true);
diff --git a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
index 2c3a224f3c8..5a5006d24c4 100644
--- a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
@@ -21,10 +21,6 @@ describe('ClustersEmptyStateComponent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the help text is not provided', () => {
beforeEach(() => {
createWrapper();
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 6f23ed47d2a..af8d3b59869 100644
--- a/spec/frontend/clusters_list/components/clusters_main_view_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
@@ -40,10 +40,6 @@ describe('ClustersMainViewComponent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTabs = () => wrapper.findComponent(GlTabs);
const findAllTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllTabs().at(index);
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 20dbff9df15..207bfddcb4f 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -75,7 +75,6 @@ describe('Clusters', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
captureException.mockRestore();
});
@@ -271,9 +270,7 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalSecondPage, perPage, 2));
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ currentPage: 2 });
+ findPaginatedButtons().vm.$emit('input', 2);
return axios.waitForAll();
});
diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
index b4eb9242003..e81b242dd90 100644
--- a/spec/frontend/clusters_list/components/clusters_view_all_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
@@ -60,10 +60,6 @@ describe('ClustersViewAllComponent', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when agents and clusters are not loaded', () => {
const initialState = {
loadingClusters: true,
diff --git a/spec/frontend/clusters_list/components/delete_agent_button_spec.js b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
index 82850b9dea4..53cf67bca0f 100644
--- a/spec/frontend/clusters_list/components/delete_agent_button_spec.js
+++ b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
@@ -33,7 +33,7 @@ describe('DeleteAgentButton', () => {
const findDeleteBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findPrimaryAction = () => findModal().props('actionPrimary');
- const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr];
const findDeleteAgentButtonTooltip = () => wrapper.findByTestId('delete-agent-button-tooltip');
const getTooltipText = (el) => {
const binding = getBinding(el, 'gl-tooltip');
@@ -84,7 +84,7 @@ describe('DeleteAgentButton', () => {
...provideData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData,
mocks: { $toast: { show: toast } },
@@ -108,7 +108,6 @@ describe('DeleteAgentButton', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
deleteResponse = null;
toast = null;
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 10264d6a011..3156eaaecfc 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -139,7 +139,6 @@ describe('InstallAgentModal', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
});
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 3211ba44eff..a3dfc848fc8 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
@@ -13,10 +13,6 @@ describe('NodeErrorHelpText', () => {
const findPopover = () => wrapper.findComponent(GlPopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
errorType | wrapperText | popoverText
${'authentication_error'} | ${'Unable to Authenticate'} | ${'GitLab failed to authenticate'}
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 360fd3b2842..6d23db0517d 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -5,13 +5,13 @@ import waitForPromises from 'helpers/wait_for_promises';
import { MAX_REQUESTS } from '~/clusters_list/constants';
import * as actions from '~/clusters_list/store/actions';
import * as types from '~/clusters_list/store/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { apiData } from '../mock_data';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Clusters store actions', () => {
let captureException;
@@ -81,7 +81,7 @@ describe('Clusters store actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index b9be262efd0..88861b0d08a 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -32,10 +32,6 @@ function factory(initialState = {}, props = {}) {
}
describe('Code navigation app component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets initial data on mount if the correct props are passed', () => {
const codeNavigationPath = 'code/nav/path.js';
const path = 'blob/path.js';
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index 874263e046a..1bfaf7e959e 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -61,10 +61,6 @@ function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }
}
describe('Code navigation popover component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders popover', () => {
factory({
position: { x: 0, y: 0, height: 0 },
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 debd10de118..64623968aa0 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -5,7 +5,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
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';
@@ -20,7 +20,7 @@ import {
mockUpstreamQueryResponse,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -69,10 +69,6 @@ describe('Commit box pipeline mini graph', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('should display loading state when loading', () => {
createComponent();
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index e75fb697a7b..e474ef9c635 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Poll from '~/lib/utils/poll';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/poll');
jest.mock('visibilityjs');
-jest.mock('~/flash');
+jest.mock('~/alert');
const mockFetchData = jest.fn();
jest.mock('~/projects/tree/services/commit_pipeline_service', () =>
@@ -41,11 +41,6 @@ describe('Commit pipeline status component', () => {
const findLink = () => wrapper.find('a');
const findCiIcon = () => findLink().findComponent(CiIcon);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Visibility management', () => {
describe('when component is hidden', () => {
beforeEach(() => {
@@ -169,7 +164,7 @@ describe('Commit pipeline status component', () => {
});
});
- it('displays flash error message', () => {
+ it('displays alert error message', () => {
expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index 8d455f8a3d7..9c7a41b3506 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue';
import {
@@ -23,7 +23,7 @@ const mockProvide = {
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Commit box pipeline status', () => {
let wrapper;
@@ -54,10 +54,6 @@ describe('Commit box pipeline status', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('should display loading state when loading', () => {
createComponent();
diff --git a/spec/frontend/commit/components/signature_badge_spec.js b/spec/frontend/commit/components/signature_badge_spec.js
new file mode 100644
index 00000000000..d52ad2b43e2
--- /dev/null
+++ b/spec/frontend/commit/components/signature_badge_spec.js
@@ -0,0 +1,134 @@
+import { GlBadge, GlLink, GlPopover } from '@gitlab/ui';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import SignatureBadge from '~/commit/components/signature_badge.vue';
+import X509CertificateDetails from '~/commit/components/x509_certificate_details.vue';
+import { typeConfig, statusConfig, verificationStatuses, signatureTypes } from '~/commit/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { sshSignatureProp, gpgSignatureProp, x509SignatureProp } from '../mock_data';
+
+describe('Commit signature', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(SignatureBadge, {
+ propsData: {
+ signature: {
+ ...props,
+ },
+ stubs: {
+ GlBadge,
+ GlLink,
+ X509CertificateDetails,
+ GlPopover: stubComponent(GlPopover, { template: RENDER_ALL_SLOTS_TEMPLATE }),
+ },
+ },
+ });
+ };
+
+ const signatureBadge = () => wrapper.findComponent(GlBadge);
+ const signaturePopover = () => wrapper.findComponent(GlPopover);
+ const signatureDescription = () => wrapper.findByTestId('signature-description');
+ const signatureKeyLabel = () => wrapper.findByTestId('signature-key-label');
+ const signatureKey = () => wrapper.findByTestId('signature-key');
+ const helpLink = () => wrapper.findComponent(GlLink);
+ const X509CertificateDetailsComponents = () => wrapper.findAllComponents(X509CertificateDetails);
+
+ describe.each`
+ signatureType | verificationStatus
+ ${signatureTypes.GPG} | ${verificationStatuses.VERIFIED}
+ ${signatureTypes.GPG} | ${verificationStatuses.UNVERIFIED}
+ ${signatureTypes.GPG} | ${verificationStatuses.UNVERIFIED_KEY}
+ ${signatureTypes.GPG} | ${verificationStatuses.UNKNOWN_KEY}
+ ${signatureTypes.GPG} | ${verificationStatuses.OTHER_USER}
+ ${signatureTypes.GPG} | ${verificationStatuses.SAME_USER_DIFFERENT_EMAIL}
+ ${signatureTypes.GPG} | ${verificationStatuses.MULTIPLE_SIGNATURES}
+ ${signatureTypes.X509} | ${verificationStatuses.VERIFIED}
+ ${signatureTypes.SSH} | ${verificationStatuses.VERIFIED}
+ ${signatureTypes.SSH} | ${verificationStatuses.REVOKED_KEY}
+ `(
+ 'For a specified `$signatureType` and `$verificationStatus` it renders component correctly',
+ ({ signatureType, verificationStatus }) => {
+ beforeEach(() => {
+ createComponent({ __typename: signatureType, verificationStatus });
+ });
+ it('renders correct badge class', () => {
+ expect(signatureBadge().props('variant')).toBe(statusConfig[verificationStatus].variant);
+ });
+ it('renders badge text', () => {
+ expect(signatureBadge().text()).toBe(statusConfig[verificationStatus].label);
+ });
+ it('renders popover header text', () => {
+ expect(signaturePopover().text()).toMatch(statusConfig[verificationStatus].title);
+ });
+ it('renders signature description', () => {
+ expect(signatureDescription().text()).toBe(statusConfig[verificationStatus].description);
+ });
+ it('renders help link with correct path', () => {
+ expect(helpLink().text()).toBe(typeConfig[signatureType].helpLink.label);
+ expect(helpLink().attributes('href')).toBe(
+ helpPagePath(typeConfig[signatureType].helpLink.path),
+ );
+ });
+ },
+ );
+
+ describe('SSH signature', () => {
+ beforeEach(() => {
+ createComponent(sshSignatureProp);
+ });
+
+ it('renders key label', () => {
+ expect(signatureKeyLabel().text()).toMatch(typeConfig[signatureTypes.SSH].keyLabel);
+ });
+
+ it('renders key signature', () => {
+ expect(signatureKey().text()).toBe(sshSignatureProp.keyFingerprintSha256);
+ });
+ });
+
+ describe('GPG signature', () => {
+ beforeEach(() => {
+ createComponent(gpgSignatureProp);
+ });
+
+ it('renders key label', () => {
+ expect(signatureKeyLabel().text()).toMatch(typeConfig[signatureTypes.GPG].keyLabel);
+ });
+
+ it('renders key signature for GGP signature', () => {
+ expect(signatureKey().text()).toBe(gpgSignatureProp.gpgKeyPrimaryKeyid);
+ });
+ });
+
+ describe('X509 signature', () => {
+ beforeEach(() => {
+ createComponent(x509SignatureProp);
+ });
+
+ it('does not render key label', () => {
+ expect(signatureKeyLabel().exists()).toBe(false);
+ });
+
+ it('renders X509 certificate details components', () => {
+ expect(X509CertificateDetailsComponents()).toHaveLength(2);
+ });
+
+ it('passes correct props', () => {
+ expect(X509CertificateDetailsComponents().at(0).props()).toStrictEqual({
+ subject: x509SignatureProp.x509Certificate.subject,
+ title: typeConfig[signatureTypes.X509].subjectTitle,
+ subjectKeyIdentifier: wrapper.vm.getSubjectKeyIdentifierToDisplay(
+ x509SignatureProp.x509Certificate.subjectKeyIdentifier,
+ ),
+ });
+ expect(X509CertificateDetailsComponents().at(1).props()).toStrictEqual({
+ subject: x509SignatureProp.x509Certificate.x509Issuer.subject,
+ title: typeConfig[signatureTypes.X509].issuerTitle,
+ subjectKeyIdentifier: wrapper.vm.getSubjectKeyIdentifierToDisplay(
+ x509SignatureProp.x509Certificate.x509Issuer.subjectKeyIdentifier,
+ ),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/commit/components/x509_certificate_details_spec.js b/spec/frontend/commit/components/x509_certificate_details_spec.js
new file mode 100644
index 00000000000..5d9398b572b
--- /dev/null
+++ b/spec/frontend/commit/components/x509_certificate_details_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import X509CertificateDetails from '~/commit/components/x509_certificate_details.vue';
+import { X509_CERTIFICATE_KEY_IDENTIFIER_TITLE } from '~/commit/constants';
+import { x509CertificateDetailsProp } from '../mock_data';
+
+describe('X509 certificate details', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(X509CertificateDetails, {
+ propsData: x509CertificateDetailsProp,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findTitle = () => wrapper.find('strong');
+ const findSubjectValues = () => wrapper.findAll("[data-testid='subject-value']");
+ const findKeyIdentifier = () => wrapper.find("[data-testid='key-identifier']");
+
+ it('renders a title', () => {
+ expect(findTitle().text()).toBe(x509CertificateDetailsProp.title);
+ });
+
+ it('renders subject values', () => {
+ expect(findSubjectValues()).toHaveLength(3);
+ });
+
+ it('renders key identifier', () => {
+ expect(findKeyIdentifier().text()).toBe(
+ `${X509_CERTIFICATE_KEY_IDENTIFIER_TITLE} ${x509CertificateDetailsProp.subjectKeyIdentifier}`,
+ );
+ });
+});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index a13ef9c563e..3b6971d9607 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -201,3 +201,34 @@ export const mockUpstreamQueryResponse = {
},
},
};
+
+export const sshSignatureProp = {
+ __typename: 'SshSignature',
+ verificationStatus: 'VERIFIED',
+ keyFingerprintSha256: 'xxx',
+};
+
+export const gpgSignatureProp = {
+ __typename: 'GpgSignature',
+ verificationStatus: 'VERIFIED',
+ gpgKeyPrimaryKeyid: 'yyy',
+};
+
+export const x509SignatureProp = {
+ __typename: 'X509Signature',
+ verificationStatus: 'VERIFIED',
+ x509Certificate: {
+ subject: 'CN=gitlab@example.org,OU=Example,O=World',
+ subjectKeyIdentifier: 'BC:BC:BC:BC:BC:BC:BC:BC',
+ x509Issuer: {
+ subject: 'CN=PKI,OU=Example,O=World',
+ subjectKeyIdentifier: 'AB:AB:AB:AB:AB:AB:AB:AB:',
+ },
+ },
+};
+
+export const x509CertificateDetailsProp = {
+ title: 'Title',
+ subject: 'CN=gitlab@example.org,OU=Example,O=World',
+ subjectKeyIdentifier: 'BC BC BC BC BC BC BC BC',
+};
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 4bffb6a0fd3..009ec68ddcf 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
@@ -13,7 +14,7 @@ import {
HTTP_STATUS_OK,
HTTP_STATUS_UNAUTHORIZED,
} from '~/lib/utils/http_status';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
@@ -21,12 +22,13 @@ const $toast = {
show: jest.fn(),
};
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Pipelines table in Commits and Merge requests', () => {
let wrapper;
let pipeline;
let mock;
+ const showMock = jest.fn();
const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button');
const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile');
@@ -38,7 +40,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link');
- const createComponent = (props = {}) => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
mount(PipelinesTable, {
propsData: {
@@ -50,6 +52,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
mocks: {
$toast,
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: '<div />',
+ methods: { show: showMock },
+ }),
+ },
}),
);
};
@@ -62,11 +70,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
});
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
describe('successful request', () => {
describe('without pipelines', () => {
beforeEach(async () => {
@@ -95,6 +98,35 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
});
+ describe('with pagination', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], {
+ 'X-TOTAL': 10,
+ 'X-PER-PAGE': 2,
+ 'X-PAGE': 1,
+ 'X-TOTAL-PAGES': 5,
+ 'X-NEXT-PAGE': 2,
+ 'X-PREV-PAGE': 2,
+ });
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should make an API request when using pagination', async () => {
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0].params.page).toBe('1');
+
+ wrapper.find('.next-page-item').trigger('click');
+
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(2);
+ expect(mock.history.get[1].params.page).toBe('2');
+ });
+ });
+
describe('with pipelines', () => {
beforeEach(async () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'x-total': 10 });
@@ -111,32 +143,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(findErrorEmptyState().exists()).toBe(false);
});
- describe('with pagination', () => {
- it('should make an API request when using pagination', async () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({
- store: {
- state: {
- pageInfo: {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- },
- },
- },
- });
-
- wrapper.find('.next-page-item').trigger('click');
-
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ page: '2' });
- });
- });
-
describe('pipeline badge counts', () => {
it('should receive update-pipelines-count event', () => {
const element = document.createElement('div');
@@ -203,16 +209,18 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
- canRunPipeline: true,
- projectId: '5',
- mergeRequestId: 3,
+ props: {
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
+ },
});
await waitForPromises();
});
describe('success', () => {
beforeEach(() => {
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue();
});
it('displays a toast message during pipeline creation', async () => {
await findRunPipelineBtn().trigger('click');
@@ -255,9 +263,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
`('displays permissions error message', async ({ status, message }) => {
const response = { response: { status } };
- jest
- .spyOn(Api, 'postMergeRequestPipeline')
- .mockImplementation(() => Promise.reject(response));
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockRejectedValue(response);
await findRunPipelineBtn().trigger('click');
@@ -281,14 +287,16 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
- projectId: '5',
- mergeRequestId: 3,
- canCreatePipelineInTargetProject: true,
- sourceProjectFullPath: 'test/parent-project',
- targetProjectFullPath: 'test/fork-project',
+ props: {
+ projectId: '5',
+ mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
+ },
});
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue();
await waitForPromises();
});
@@ -313,15 +321,15 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []);
createComponent({
- projectId: '5',
- mergeRequestId: 3,
- canCreatePipelineInTargetProject: true,
- sourceProjectFullPath: 'test/parent-project',
- targetProjectFullPath: 'test/fork-project',
+ props: {
+ projectId: '5',
+ mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
+ },
});
- jest.spyOn(findModal().vm, 'show').mockReturnValue();
-
await waitForPromises();
});
@@ -331,7 +339,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
findRunPipelineBtn().trigger('click');
- expect(findModal().vm.show).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
index d6f16f1a644..a7ae07a36d9 100644
--- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -46,7 +46,6 @@ function factory(projects = mockData) {
describe('Confidential merge request project form group component', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
it('renders fork dropdown', async () => {
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
index a63cca006da..b8e6bcbc3c4 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = `
-"<b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
+"<b-button-stub size=\\"md\\" tag=\\"button\\" type=\\"button\\" variant=\\"default\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
<!---->
<gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub>
<!---->
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
index 0700cf5d529..271e63abf21 100644
--- a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
@@ -51,10 +51,6 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
setupMocks();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('initializes BubbleMenuPlugin', async () => {
createWrapper({});
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
index 378b11f4ae9..085a6d3a28d 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
@@ -64,10 +64,6 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
buildWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders bubble menu component', async () => {
tiptapEditor.commands.insertContent(preTag());
bubbleMenu = wrapper.findComponent(BubbleMenu);
diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
index 98001858851..7bab473529f 100644
--- a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
@@ -37,10 +37,6 @@ describe('content_editor/components/bubble_menus/formatting_bubble_menu', () =>
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders bubble menu component', () => {
buildWrapper();
const bubbleMenu = wrapper.findComponent(BubbleMenu);
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
index 9aa9c6483f4..eb5a3b61591 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
@@ -71,10 +71,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
.run();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders bubble menu component', async () => {
await buildWrapperAndDisplayMenu();
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
index 13c6495ac41..c918f068c07 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
@@ -4,22 +4,28 @@ 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';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
+import Image from '~/content_editor/extensions/image';
import Video from '~/content_editor/extensions/video';
import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+ PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../../test_constants';
-const TIPTAP_IMAGE_HTML = `<p>
+const TIPTAP_AUDIO_HTML = `<p>
+ <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+</p>`;
+
+const TIPTAP_DIAGRAM_HTML = `<p>
<img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
-const TIPTAP_AUDIO_HTML = `<p>
- <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+const TIPTAP_IMAGE_HTML = `<p>
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_VIDEO_HTML = `<p>
@@ -29,10 +35,11 @@ const TIPTAP_VIDEO_HTML = `<p>
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
describe.each`
- mediaType | mediaHTML | filePath | mediaOutputHTML
- ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
- ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
- ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
+ mediaType | mediaHTML | filePath | mediaOutputHTML
+ ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
+ ${'drawio_diagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${'test-file.drawio.svg'} | ${TIPTAP_DIAGRAM_HTML}
+ ${'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_bubble_menu ($mediaType)',
({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
@@ -43,7 +50,7 @@ describe.each`
let eventHub;
const buildEditor = () => {
- tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] });
+ tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video, DrawioDiagram] });
contentEditor = { resolveUrl: jest.fn() };
eventHub = eventHubFactory();
};
@@ -93,10 +100,6 @@ describe.each`
bubbleMenu = wrapper.findComponent(BubbleMenu);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders bubble menu component', async () => {
expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
@@ -114,6 +117,24 @@ describe.each`
expect(link.text()).toBe(filePath);
});
+ describe('when BubbleMenu emits hidden event', () => {
+ it('resets media bubble menu state', async () => {
+ // Switch to edit mode to access component state in form fields
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ const mediaSrcInput = wrapper.findByTestId('media-src').vm.$el;
+ const mediaAltInput = wrapper.findByTestId('media-alt').vm.$el;
+
+ expect(mediaSrcInput.value).not.toBe('');
+ expect(mediaAltInput.value).not.toBe('');
+
+ await wrapper.findComponent(BubbleMenu).vm.$emit('hidden');
+
+ expect(mediaSrcInput.value).toBe('');
+ expect(mediaAltInput.value).toBe('');
+ });
+ });
+
describe('copy button', () => {
it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
jest.spyOn(navigator.clipboard, 'writeText');
@@ -133,23 +154,39 @@ describe.each`
});
describe(`replace ${mediaType} button`, () => {
- it('uploads and replaces the selected image when file input changes', async () => {
- const commands = mockChainedCommands(tiptapEditor, [
- 'focus',
- 'deleteSelection',
- 'uploadAttachment',
- 'run',
- ]);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
-
- await wrapper.findByTestId('replace-media').vm.$emit('click');
- await selectFile(file);
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.deleteSelection).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
- expect(commands.run).toHaveBeenCalled();
- });
+ if (mediaType !== 'drawio_diagram') {
+ it('uploads and replaces the selected image when file input changes', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'deleteSelection',
+ 'uploadAttachment',
+ 'run',
+ ]);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await wrapper.findByTestId('replace-media').vm.$emit('click');
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.deleteSelection).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ } else {
+ // draw.io diagrams are replaced using the edit diagram button
+ it('invokes editDiagram command', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'createOrEditDiagram',
+ 'run',
+ ]);
+ await wrapper.findByTestId('edit-diagram').vm.$emit('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.createOrEditDiagram).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
+ });
+ }
});
describe('edit button', () => {
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 ee9ead8f8a7..e62e2331d25 100644
--- a/spec/frontend/content_editor/components/content_editor_alert_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -29,10 +29,6 @@ describe('content_editor/components/content_editor_alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
variant | message
${'danger'} | ${'An error occurred'}
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 1a3cd36a8bb..b642ac9c46b 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,7 +1,8 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { EditorContent, Editor } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
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';
@@ -27,19 +28,23 @@ describe('ContentEditor', () => {
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
- const createWrapper = ({ markdown, autofocus, useBottomToolbar } = {}) => {
+ const createWrapper = ({ markdown, autofocus, ...props } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
markdown,
autofocus,
- useBottomToolbar,
+ placeholder: 'Enter some text here...',
+ ...props,
},
stubs: {
EditorStateObserver,
ContentEditorProvider,
ContentEditorAlert,
+ GlLink,
+ GlSprintf,
+ EditorModeDropdown,
},
});
};
@@ -48,10 +53,6 @@ describe('ContentEditor', () => {
renderMarkdown = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('triggers initialized event', () => {
createWrapper();
@@ -87,22 +88,29 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
- it('renders top toolbar component', () => {
+ it('renders toolbar component', () => {
createWrapper();
expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(false);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(true);
});
- it('renders bottom toolbar component', () => {
- createWrapper({
- useBottomToolbar: true,
- });
+ it('renders footer containing quick actions help text if quick actions docs path is defined', () => {
+ createWrapper({ quickActionsDocsPath: '/foo/bar' });
- expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(true);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(false);
+ expect(findEditorElement().text()).toContain('For quick actions, type /');
+ expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar');
+ });
+
+ it('does not render footer containing quick actions help text if quick actions docs path is not defined', () => {
+ createWrapper();
+
+ expect(findEditorElement().text()).not.toContain('For quick actions, type /');
+ });
+
+ it('renders an editor mode dropdown', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(EditorModeDropdown).exists()).toBe(true);
});
describe('when setting initial content', () => {
@@ -124,9 +132,9 @@ describe('ContentEditor', () => {
describe('succeeds', () => {
beforeEach(async () => {
- renderMarkdown.mockResolvedValueOnce('hello world');
+ renderMarkdown.mockResolvedValueOnce('');
- createWrapper({ markddown: 'hello world' });
+ createWrapper({ markddown: '' });
await nextTick();
});
@@ -138,13 +146,17 @@ describe('ContentEditor', () => {
it('emits loadingSuccess event', () => {
expect(wrapper.emitted('loadingSuccess')).toHaveLength(1);
});
+
+ it('shows placeholder text', () => {
+ expect(wrapper.text()).toContain('Enter some text here...');
+ });
});
describe('fails', () => {
beforeEach(async () => {
renderMarkdown.mockRejectedValueOnce(new Error());
- createWrapper({ markddown: 'hello world' });
+ createWrapper({ markdown: 'hello world' });
await nextTick();
});
@@ -209,11 +221,17 @@ describe('ContentEditor', () => {
expect(findEditorElement().classes()).not.toContain('is-focused');
});
+
+ it('hides placeholder text', () => {
+ expect(wrapper.text()).not.toContain('Enter some text here...');
+ });
});
describe('when editorStateObserver emits docUpdate event', () => {
- it('emits change event with the latest markdown', async () => {
- const markdown = 'Loaded content';
+ let markdown;
+
+ beforeEach(async () => {
+ markdown = 'Loaded content';
renderMarkdown.mockResolvedValueOnce(markdown);
@@ -223,7 +241,9 @@ describe('ContentEditor', () => {
await waitForPromises();
findEditorStateObserver().vm.$emit('docUpdate');
+ });
+ it('emits change event with the latest markdown', () => {
expect(wrapper.emitted('change')).toEqual([
[
{
@@ -234,6 +254,10 @@ describe('ContentEditor', () => {
],
]);
});
+
+ it('hides the placeholder text', () => {
+ expect(wrapper.text()).not.toContain('Enter some text here...');
+ });
});
describe('when editorStateObserver emits keydown event', () => {
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 9b42f61c98c..80fb20e5258 100644
--- a/spec/frontend/content_editor/components/editor_state_observer_spec.js
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -45,10 +45,6 @@ describe('content_editor/components/editor_state_observer', () => {
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when editor content changes', () => {
it('emits update, selectionUpdate, and transaction events', () => {
const content = '<p>My paragraph</p>';
diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index c4bf21ba813..4a7b7cedf19 100644
--- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -1,3 +1,4 @@
+import { GlTabs, GlTab } from '@gitlab/ui';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
@@ -6,22 +7,23 @@ import {
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-describe('content_editor/components/top_toolbar', () => {
+describe('content_editor/components/formatting_toolbar', () => {
let wrapper;
let trackingSpy;
const buildWrapper = () => {
- wrapper = shallowMountExtended(FormattingToolbar);
+ wrapper = shallowMountExtended(FormattingToolbar, {
+ stubs: {
+ GlTabs,
+ GlTab,
+ },
+ });
};
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
testId | controlProps
${'text-styles'} | ${{}}
diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js
index 0065103d01b..1b0ffaee6c6 100644
--- a/spec/frontend/content_editor/components/loading_indicator_spec.js
+++ b/spec/frontend/content_editor/components/loading_indicator_spec.js
@@ -11,10 +11,6 @@ describe('content_editor/components/loading_indicator', () => {
wrapper = shallowMountExtended(LoadingIndicator);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading content', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index 1f1f7b338c6..1556f761682 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -42,10 +42,6 @@ describe('content_editor/components/toolbar_button', () => {
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays tertiary, medium button with a provided label and icon', () => {
buildWrapper();
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 5473d43f5a1..0ec950137fc 100644
--- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
@@ -50,7 +50,6 @@ describe('content_editor/components/toolbar_image_button', () => {
afterEach(() => {
editor.destroy();
- wrapper.destroy();
});
it('sets the image to the value in the URL input when "Insert" button is clicked', async () => {
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 40e859e96af..80090c0278f 100644
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -43,7 +43,6 @@ describe('content_editor/components/toolbar_link_button', () => {
afterEach(() => {
editor.destroy();
- wrapper.destroy();
});
it('renders dropdown component', () => {
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 d4fc47601cf..5af4784f358 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -9,12 +9,14 @@ import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_
describe('content_editor/components/toolbar_more_dropdown', () => {
let wrapper;
let tiptapEditor;
+ let contentEditor;
let eventHub;
const buildEditor = () => {
tiptapEditor = createTestEditor({
extensions: [Diagram, HorizontalRule],
});
+ contentEditor = { drawioEnabled: true };
eventHub = eventHubFactory();
};
@@ -22,6 +24,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
wrapper = mountExtended(ToolbarMoreDropdown, {
provide: {
tiptapEditor,
+ contentEditor,
eventHub,
},
propsData,
@@ -32,29 +35,27 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
beforeEach(() => {
buildEditor();
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
});
describe.each`
- name | contentType | command | params
- ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
- ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
- ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
- ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
- ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
- ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
- ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
- ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
- ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
+ name | contentType | command | params
+ ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
+ ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
+ ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
+ ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
+ ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
+ ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
+ ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
+ ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
+ ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
+ ${'Create or edit diagram'} | ${'drawioDiagram'} | ${'createOrEditDiagram'} | ${[]}
`('when option $name is clicked', ({ name, command, contentType, params }) => {
let commands;
let btn;
beforeEach(async () => {
+ buildWrapper();
+
commands = mockChainedCommands(tiptapEditor, [command, 'focus', 'run']);
btn = wrapper.findByRole('button', { name });
});
@@ -71,8 +72,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
});
+ it('does not show drawio option when drawio is disabled', () => {
+ contentEditor.drawioEnabled = false;
+ buildWrapper();
+
+ expect(wrapper.findByRole('button', { name: 'Create or edit diagram' }).exists()).toBe(false);
+ });
+
describe('a11y tests', () => {
it('sets toggleText and text-sr-only properties to the table button dropdown', () => {
+ buildWrapper();
+
expect(findDropdown().props()).toMatchObject({
textSrOnly: true,
toggleText: 'More options',
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 aa4604661e5..35741971488 100644
--- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
@@ -30,7 +30,6 @@ describe('content_editor/components/toolbar_table_button', () => {
afterEach(() => {
editor.destroy();
- wrapper.destroy();
});
it('renders a grid of 5x5 buttons to create a 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 5a725ac1ca4..31ed13541e6 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
@@ -39,10 +39,6 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all text styles as dropdown items', () => {
buildWrapper();
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 a5ef19fb8e8..057e50cd0e2 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -55,10 +55,6 @@ describe('content/components/wrappers/code_block', () => {
codeBlockLanguageLoader.findOrCreateLanguageBySyntax.mockReturnValue({ syntax: language });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a node-view-wrapper as a pre element', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js
index d746b9fa2f1..232c1e9aede 100644
--- a/spec/frontend/content_editor/components/wrappers/details_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/details_spec.js
@@ -13,10 +13,6 @@ describe('content/components/wrappers/details', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a node-view-content as a ul element', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
index 1ff750eb2ac..91c6799478e 100644
--- a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
@@ -12,10 +12,6 @@ describe('content/components/wrappers/footnote_definition', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders footnote label as a readyonly element', () => {
const label = 'footnote';
diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/label_spec.js
index 9e58669b0ea..fa32b746142 100644
--- a/spec/frontend/content_editor/components/wrappers/label_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/label_spec.js
@@ -11,10 +11,6 @@ describe('content/components/wrappers/label', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it("renders a GlLabel with the node's text and color", () => {
createWrapper({ attrs: { color: '#ff0000', text: 'foo bar', originalText: '~"foo bar"' } });
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index 1fdddce3962..d8f34565705 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -1,12 +1,12 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
-import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables';
+import { selectedRect as getSelectedRect } from '@tiptap/pm/tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
-jest.mock('@_ueberdosis/prosemirror-tables');
+jest.mock('@tiptap/pm/tables');
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
@@ -52,10 +52,6 @@ describe('content/components/wrappers/table_cell_base', () => {
editor = createTestEditor({});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a td node-view-wrapper with relative position', () => {
createWrapper();
expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
index 2aefbc77545..506f442bcc7 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -22,10 +22,6 @@ describe('content/components/wrappers/table_cell_body', () => {
editor = createTestEditor({});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a TableCellBase component', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
index e48df8734a6..bebe7fb4124 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -22,10 +22,6 @@ describe('content/components/wrappers/table_cell_header', () => {
editor = createTestEditor({});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a TableCellBase component', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
diff --git a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
index bfda89a8b09..4d5911dda0c 100644
--- a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
@@ -70,10 +70,6 @@ describe('content/components/wrappers/table_of_contents', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a node-view-wrapper as a ul element', () => {
expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('ul');
});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index 6b804b3b4c6..24b75ba6805 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -2,12 +2,13 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
-import { VARIANT_DANGER } from '~/flash';
+import { VARIANT_DANGER } from '~/alert';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
@@ -16,6 +17,7 @@ import {
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
PROJECT_WIKI_ATTACHMENT_LINK_HTML,
+ PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../test_constants';
describe('content_editor/extensions/attachment', () => {
@@ -24,6 +26,7 @@ describe('content_editor/extensions/attachment', () => {
let p;
let image;
let audio;
+ let drawioDiagram;
let video;
let loading;
let link;
@@ -35,6 +38,7 @@ describe('content_editor/extensions/attachment', () => {
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' });
const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' });
+ const drawioDiagramFile = new File(['foo'], 'test-file.drawio.svg', { type: 'image/svg+xml' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
@@ -67,12 +71,13 @@ describe('content_editor/extensions/attachment', () => {
Image,
Audio,
Video,
+ DrawioDiagram,
Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
],
});
({
- builders: { doc, p, image, audio, video, loading, link },
+ builders: { doc, p, image, audio, video, loading, link, drawioDiagram },
} = createDocBuilder({
tiptapEditor,
names: {
@@ -81,6 +86,7 @@ describe('content_editor/extensions/attachment', () => {
link: { nodeType: Link.name },
audio: { nodeType: Audio.name },
video: { nodeType: Video.name },
+ drawioDiagram: { nodeType: DrawioDiagram.name },
},
}));
@@ -113,10 +119,11 @@ describe('content_editor/extensions/attachment', () => {
});
describe.each`
- nodeType | mimeType | html | file | mediaType
- ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
- ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
- ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
+ nodeType | mimeType | html | file | mediaType
+ ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
+ ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
+ ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
+ ${'drawioDiagram'} | ${'image/svg+xml'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${drawioDiagramFile} | ${(attrs) => drawioDiagram(attrs)}
`('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => {
const base64EncodedFile = `data:${mimeType};base64,Zm9v`;
@@ -151,7 +158,7 @@ describe('content_editor/extensions/attachment', () => {
mediaType({
canonicalSrc: file.name,
src: base64EncodedFile,
- alt: 'test-file',
+ alt: expect.stringContaining('test-file'),
uploading: false,
}),
),
diff --git a/spec/frontend/content_editor/extensions/drawio_diagram_spec.js b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
new file mode 100644
index 00000000000..61dc164c99a
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
@@ -0,0 +1,103 @@
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
+import Image from '~/content_editor/extensions/image';
+import createAssetResolver from '~/content_editor/services/asset_resolver';
+import { create } from '~/drawio/content_editor_facade';
+import { launchDrawioEditor } from '~/drawio/drawio_editor';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
+} from '../test_constants';
+
+jest.mock('~/content_editor/services/asset_resolver');
+jest.mock('~/drawio/content_editor_facade');
+jest.mock('~/drawio/drawio_editor');
+
+describe('content_editor/extensions/drawio_diagram', () => {
+ let tiptapEditor;
+ let doc;
+ let paragraph;
+ let image;
+ let drawioDiagram;
+ const uploadsPath = '/uploads';
+ const renderMarkdown = () => {};
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({
+ extensions: [Image, DrawioDiagram.configure({ uploadsPath, renderMarkdown })],
+ });
+ const { builders } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ image: { nodeType: Image.name },
+ drawioDiagram: { nodeType: DrawioDiagram.name },
+ },
+ });
+
+ doc = builders.doc;
+ paragraph = builders.paragraph;
+ image = builders.image;
+ drawioDiagram = builders.drawioDiagram;
+ });
+
+ describe('parsing', () => {
+ it('distinguishes a drawio diagram from an image', () => {
+ const expectedDocWithDiagram = doc(
+ paragraph(
+ drawioDiagram({
+ alt: 'test-file',
+ canonicalSrc: 'test-file.drawio.svg',
+ src: '/group1/project1/-/wikis/test-file.drawio.svg',
+ }),
+ ),
+ );
+ const expectedDocWithImage = doc(
+ paragraph(
+ image({
+ alt: 'test-file',
+ canonicalSrc: 'test-file.png',
+ src: '/group1/project1/-/wikis/test-file.png',
+ }),
+ ),
+ );
+ tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML);
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithDiagram.toJSON());
+
+ tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithImage.toJSON());
+ });
+ });
+
+ describe('createOrEditDiagram command', () => {
+ let editorFacade;
+ let assetResolver;
+
+ beforeEach(() => {
+ editorFacade = {};
+ assetResolver = {};
+ tiptapEditor.commands.createOrEditDiagram();
+
+ create.mockReturnValueOnce(editorFacade);
+ createAssetResolver.mockReturnValueOnce(assetResolver);
+ });
+
+ it('creates a new instance of asset resolver', () => {
+ expect(createAssetResolver).toHaveBeenCalledWith({ renderMarkdown });
+ });
+
+ it('creates a new instance of the content_editor_facade', () => {
+ expect(create).toHaveBeenCalledWith({
+ tiptapEditor,
+ drawioNodeName: DrawioDiagram.name,
+ uploadsPath,
+ assetResolver,
+ });
+ });
+
+ it('calls launchDrawioEditor and provides content_editor_facade', () => {
+ expect(launchDrawioEditor).toHaveBeenCalledWith({ editorFacade });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
index 30e798e8817..8f3a4934e77 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
@@ -3,7 +3,7 @@ import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
import Bold from '~/content_editor/extensions/bold';
-import { VARIANT_DANGER } from '~/flash';
+import { VARIANT_DANGER } from '~/alert';
import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
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 5df901e0f15..bf29d4bdf23 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,4 +1,4 @@
-import { DOMSerializer } from 'prosemirror-model';
+import { DOMSerializer } from '@tiptap/pm/model';
import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import { createTiptapEditor } from 'jest/content_editor/test_utils';
diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js
index e1a30819ac8..00cc628ca72 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -20,7 +20,7 @@ describe('content_editor/services/create_content_editor', () => {
preserveUnchangedMarkdown: false,
},
};
- editor = createContentEditor({ renderMarkdown, uploadsPath });
+ editor = createContentEditor({ renderMarkdown, uploadsPath, drawioEnabled: true });
});
describe('when preserveUnchangedMarkdown feature is on', () => {
@@ -45,10 +45,10 @@ describe('content_editor/services/create_content_editor', () => {
});
});
- it('sets gl-outline-0! class selector to the tiptapEditor instance', () => {
+ it('sets gl-shadow-none! class selector to the tiptapEditor instance', () => {
expect(editor.tiptapEditor.options.editorProps).toMatchObject({
attributes: {
- class: 'gl-outline-0!',
+ class: 'gl-shadow-none!',
},
});
});
@@ -82,4 +82,14 @@ describe('content_editor/services/create_content_editor', () => {
renderMarkdown,
});
});
+
+ it('provides uploadsPath and renderMarkdown function to DrawioDiagram extension', () => {
+ expect(
+ editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'drawioDiagram')
+ .options,
+ ).toMatchObject({
+ uploadsPath,
+ renderMarkdown,
+ });
+ });
});
diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index 90d83820c70..8ee37282ee9 100644
--- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -35,12 +35,10 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- renderMarkdown.mockResolvedValueOnce(
- `<p><strong>${text}</strong></p><pre lang="javascript"></pre><!-- some comment -->`,
- );
+ renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p><!-- some comment -->`);
result = await deserializer.deserialize({
- content: 'content',
+ markdown: '**Bold text**\n<!-- some comment -->',
schema: tiptapEditor.schema,
});
});
@@ -53,12 +51,22 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
});
describe('when the render function returns an empty value', () => {
- it('returns an empty object', async () => {
- const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
+ it('returns an empty prosemirror document', async () => {
+ const deserializer = createMarkdownDeserializer({
+ render: renderMarkdown,
+ schema: tiptapEditor.schema,
+ });
renderMarkdown.mockResolvedValueOnce(null);
- expect(await deserializer.deserialize({ content: 'content' })).toEqual({});
+ const result = await deserializer.deserialize({
+ markdown: '',
+ schema: tiptapEditor.schema,
+ });
+
+ const document = doc(p());
+
+ expect(result.document.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 2cd8b8a0d6f..c4d302547a5 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -8,6 +8,7 @@ import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
import FigureCaption from '~/content_editor/extensions/figure_caption';
@@ -57,6 +58,7 @@ const {
div,
descriptionItem,
descriptionList,
+ drawioDiagram,
emoji,
footnoteDefinition,
footnoteReference,
@@ -96,6 +98,7 @@ const {
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
+ drawioDiagram: { nodeType: DrawioDiagram.name },
emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name },
@@ -397,6 +400,12 @@ this is not really json:table but just trying out whether this case works or not
);
});
+ it('correctly serializes a drawio_diagram', () => {
+ expect(
+ serialize(paragraph(drawioDiagram({ src: 'diagram.drawio.svg', alt: 'Draw.io Diagram' }))),
+ ).toBe('![Draw.io Diagram](diagram.drawio.svg)');
+ });
+
it.each`
width | height | outputAttributes
${300} | ${undefined} | ${'width=300'}
diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js
index 45a0e4a8bd1..bd462ecec22 100644
--- a/spec/frontend/content_editor/test_constants.js
+++ b/spec/frontend/content_editor/test_constants.js
@@ -20,6 +20,12 @@ export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74"
</span>
</p>`;
+export const PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
+ <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.drawio.svg" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.drawio.svg">
+ <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.drawio.svg" data-canonical-src="test-file.drawio.svg">
+ </a>
+</p>`;
+
export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
<a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
</p>`;
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 0fa0e65cd26..16f90a15c24 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -17,6 +17,7 @@ import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
import Diagram from '~/content_editor/extensions/diagram';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
@@ -218,6 +219,7 @@ export const createTiptapEditor = (extensions = []) =>
DescriptionList,
Details,
DetailsContent,
+ DrawioDiagram,
Diagram,
Emoji,
FootnoteDefinition,
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 2f441f0f747..4b7439f6fd2 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -53,23 +53,22 @@ exports[`Contributors charts should render charts and a RefSelector when loading
Excluding merge commits. Limited to 6,000 commits.
</span>
- <div>
- <glareachart-stub
- annotations=""
- class="gl-mb-5"
- data="[object Object]"
- height="264"
- includelegendavgmax="true"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- thresholds=""
- width="0"
- />
- </div>
+ <glareachart-stub
+ annotations=""
+ class="gl-mb-5"
+ data="[object Object]"
+ height="264"
+ includelegendavgmax="true"
+ legendaveragetext="Avg"
+ legendcurrenttext="Current"
+ legendlayout="inline"
+ legendmaxtext="Max"
+ legendmintext="Min"
+ option="[object Object]"
+ responsive=""
+ thresholds=""
+ width="auto"
+ />
<div
class="row"
@@ -91,22 +90,21 @@ exports[`Contributors charts should render charts and a RefSelector when loading
</p>
- <div>
- <glareachart-stub
- annotations=""
- data="[object Object]"
- height="216"
- includelegendavgmax="true"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- thresholds=""
- width="0"
- />
- </div>
+ <glareachart-stub
+ annotations=""
+ data="[object Object]"
+ height="216"
+ includelegendavgmax="true"
+ legendaveragetext="Avg"
+ legendcurrenttext="Current"
+ legendlayout="inline"
+ legendmaxtext="Max"
+ legendmintext="Min"
+ option="[object Object]"
+ responsive=""
+ thresholds=""
+ width="auto"
+ />
</div>
</div>
</div>
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index 03b1e977548..f915b834aff 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
@@ -16,7 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
let wrapper;
let mock;
let store;
-const Component = Vue.extend(ContributorsCharts);
const endpoint = 'contributors/-/graphs';
const branch = 'main';
const chartData = [
@@ -32,7 +31,7 @@ function factory() {
mock.onGet().reply(HTTP_STATUS_OK, chartData);
store = createStore();
- wrapper = mountExtended(Component, {
+ wrapper = mountExtended(ContributorsCharts, {
propsData: {
endpoint,
branch,
@@ -60,7 +59,6 @@ describe('Contributors charts', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
it('should fetch chart data when mounted', () => {
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index b2ebdf2f53c..a15b9ad2978 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Contributors store actions', () => {
describe('fetchChartData', () => {
@@ -38,7 +38,7 @@ describe('Contributors store actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
index 50b432943fb..2fb6940a415 100644
--- a/spec/frontend/crm/contact_form_wrapper_spec.js
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -47,7 +47,6 @@ describe('Customer relations contact form wrapper', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index ec7172434bf..63b64a6c984 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -61,7 +61,6 @@ describe('Customer relations contacts root app', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
router = null;
});
diff --git a/spec/frontend/crm/crm_form_spec.js b/spec/frontend/crm/crm_form_spec.js
index eabcf5b1b1b..fabf43ceb9d 100644
--- a/spec/frontend/crm/crm_form_spec.js
+++ b/spec/frontend/crm/crm_form_spec.js
@@ -188,10 +188,6 @@ describe('Reusable form component', () => {
};
const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT))(
'%s form save button',
(name, { mountFunction }) => {
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index d795c585622..8408c1920a9 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -40,10 +40,6 @@ describe('Customer relations organization form wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('in edit mode', () => {
it('should render organization form with correct props', () => {
mountComponent({ isEditMode: true });
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index 1fcf6aa8f50..0b26a49a6b3 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -65,7 +65,6 @@ describe('Customer relations organizations root app', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
router = null;
});
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index 7d9ae548c9a..12fef9d5ddf 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -42,7 +42,6 @@ describe('custom metrics form fields component', () => {
});
afterEach(() => {
- wrapper.destroy();
mockAxios.restore();
});
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
index af56b94f90b..c633583f2cb 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
@@ -26,10 +26,6 @@ describe('CustomMetricsForm', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Computed', () => {
it('Form button and title text indicate the custom metric is being edited', () => {
mountComponent({ metricPersisted: true });
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 113e0d8f60d..77118ae140a 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
@@ -46,11 +46,6 @@ describe('Deploy freeze modal', () => {
wrapper.findComponent(TimezoneDropdown).trigger('input');
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Basic interactions', () => {
it('button is disabled when freeze period is invalid', () => {
expect(submitDeployFreezeButton().attributes('disabled')).toBe('true');
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 27d8fea9d5e..883cc6a344a 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -24,11 +24,6 @@ describe('Deploy freeze settings', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Deploy freeze table contains components', () => {
it('contains deploy freeze table', () => {
expect(wrapper.findComponent(DeployFreezeTable).exists()).toBe(true);
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index c2d6eb399bc..6a9e482a184 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -37,11 +37,6 @@ describe('Deploy freeze table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('dispatches fetchFreezePeriods when mounted', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchFreezePeriods');
});
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index 9b96ce5d252..d39577baa59 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -4,14 +4,14 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture } from '../helpers';
import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
jest.mock('~/api.js');
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('deploy freeze store actions', () => {
const freezePeriodFixture = freezePeriodsFixture[0];
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index d11ecf95de6..3dfb828b449 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -33,7 +33,6 @@ describe('Deploy keys app component', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 8599c55c908..5f20d4ad542 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -26,11 +26,6 @@ describe('Deploy keys key', () => {
store.keys = data;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('enabled key', () => {
const deployKey = data.enabled_keys[0];
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index f5f76d5d493..e0f86aadad4 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -23,11 +23,6 @@ describe('Deploy keys panel', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders list of keys', () => {
mountComponent();
expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length);
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
index 46f7b2f3604..a3fdab88270 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -7,20 +7,12 @@ import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/h
import { TEST_HOST } from 'helpers/test_constants';
import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
const createNewTokenPath = `${TEST_HOST}/create`;
const deployTokensHelpUrl = `${TEST_HOST}/help`;
-jest.mock('~/flash', () => {
- const original = jest.requireActual('~/flash');
-
- return {
- __esModule: true,
- ...original,
- createAlert: jest.fn(),
- };
-});
+jest.mock('~/alert');
describe('New Deploy Token', () => {
let wrapper;
@@ -43,13 +35,12 @@ describe('New Deploy Token', () => {
createNewTokenPath,
tokenType,
},
+ stubs: {
+ GlFormCheckbox,
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without a container registry', () => {
beforeEach(() => {
wrapper = factory({ containerRegistryEnabled: false });
@@ -69,7 +60,7 @@ describe('New Deploy Token', () => {
it('should show the read registry scope', () => {
const checkbox = wrapper.findAllComponents(GlFormCheckbox).at(1);
- expect(checkbox.text()).toBe('read_registry');
+ expect(checkbox.text()).toContain('read_registry');
});
function submitTokenThenCheck() {
@@ -91,7 +82,7 @@ describe('New Deploy Token', () => {
});
}
- it('should flash error message if token creation fails', async () => {
+ it('should alert error message if token creation fails', async () => {
const mockAxios = new MockAdapter(axios);
const date = new Date();
@@ -222,4 +213,32 @@ describe('New Deploy Token', () => {
return submitTokenThenCheck();
});
});
+
+ describe('help text for write_package_registry scope', () => {
+ const findWriteRegistryScopeCheckbox = () => wrapper.findAllComponents(GlFormCheckbox).at(4);
+
+ describe('with project tokenType', () => {
+ beforeEach(() => {
+ wrapper = factory();
+ });
+
+ it('should show the correct help text', () => {
+ expect(findWriteRegistryScopeCheckbox().text()).toContain(
+ 'Allows read, write and delete access to the package registry.',
+ );
+ });
+ });
+
+ describe('with group tokenType', () => {
+ beforeEach(() => {
+ wrapper = factory({ tokenType: 'group' });
+ });
+
+ it('should show the correct help text', () => {
+ expect(findWriteRegistryScopeCheckbox().text()).toContain(
+ 'Allows read and write access to the package registry.',
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/deploy_tokens/components/revoke_button_spec.js b/spec/frontend/deploy_tokens/components/revoke_button_spec.js
index fa2a7d9b155..6e81205d1c1 100644
--- a/spec/frontend/deploy_tokens/components/revoke_button_spec.js
+++ b/spec/frontend/deploy_tokens/components/revoke_button_spec.js
@@ -52,10 +52,6 @@ describe('RevokeButton', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findRevokeButton = () => wrapper.findByTestId('revoke-button');
const findModal = () => wrapper.findComponent(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('primary-revoke-btn');
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index 426a61f5a47..81e3b21a910 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -21,10 +21,6 @@ describe('Batch delete button component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders non-disabled button by default', () => {
createComponent();
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index 402e55347af..e2f1d6e4b10 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -70,6 +70,8 @@ exports[`Design note component should match the snapshot 1`] = `
>
<!---->
+
+ <!---->
</div>
</div>
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 2091e1e08dd..56bf0fa60a7 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,18 +1,22 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
-import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import destroyNoteMutation from '~/design_management/graphql/mutations/destroy_note.mutation.graphql';
+import { DELETE_NOTE_ERROR_MSG } from '~/design_management/constants';
import mockDiscussion from '../../mock_data/discussion';
import notes from '../../mock_data/notes';
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
const defaultMockDiscussion = {
id: '0',
resolved: false,
@@ -23,7 +27,6 @@ const defaultMockDiscussion = {
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
- const originalGon = window.gon;
let wrapper;
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
@@ -34,18 +37,7 @@ describe('Design discussions component', () => {
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
- const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
- const mutationVariables = {
- mutation: createNoteMutation,
- variables: {
- input: {
- noteableId: 'noteable-id',
- body: 'test',
- discussionId: '0',
- },
- },
- };
const registerPath = '/users/sign_up?redirect_to_referer=yes';
const signInPath = '/users/sign_in?redirect_to_referer=yes';
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
@@ -59,7 +51,7 @@ describe('Design discussions component', () => {
provider: { clients: { defaultClient: { readQuery } } },
};
- function createComponent(props = {}, data = {}) {
+ function createComponent({ props = {}, data = {}, apolloConfig = {} } = {}) {
wrapper = mount(DesignDiscussion, {
propsData: {
resolvedDiscussionsExpanded: true,
@@ -82,7 +74,10 @@ describe('Design discussions component', () => {
issueIid: '1',
},
mocks: {
- $apollo,
+ $apollo: {
+ ...$apollo,
+ ...apolloConfig,
+ },
$route: {
hash: '#note_1',
params: {
@@ -101,16 +96,17 @@ describe('Design discussions component', () => {
});
afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
+ confirmAction.mockReset();
});
describe('when discussion is not resolvable', () => {
beforeEach(() => {
createComponent({
- discussion: {
- ...defaultMockDiscussion,
- resolvable: false,
+ props: {
+ discussion: {
+ ...defaultMockDiscussion,
+ resolvable: false,
+ },
},
});
});
@@ -171,11 +167,13 @@ describe('Design discussions component', () => {
innerText: DEFAULT_TODO_COUNT,
});
createComponent({
- discussion: {
- ...defaultMockDiscussion,
- resolved: true,
- resolvedBy: notes[0].author,
- resolvedAt: '2020-05-08T07:10:45Z',
+ props: {
+ discussion: {
+ ...defaultMockDiscussion,
+ resolved: true,
+ resolvedBy: notes[0].author,
+ resolvedAt: '2020-05-08T07:10:45Z',
+ },
},
});
});
@@ -206,10 +204,10 @@ describe('Design discussions component', () => {
});
it('emit todo:toggle when discussion is resolved', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -261,32 +259,28 @@ describe('Design discussions component', () => {
expect(findReplyForm().exists()).toBe(true);
});
- it('calls mutation on submitting form and closes the form', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ it('closes the form when note submit mutation is completed', async () => {
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
- findReplyForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ findReplyForm().vm.$emit('note-submit-complete', { data: { createNote: {} } });
- await mutate();
await nextTick();
expect(findReplyForm().exists()).toBe(false);
});
it('clears the discussion comment on closing comment form', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
await nextTick();
findReplyForm().vm.$emit('cancel-form');
- expect(wrapper.vm.discussionComment).toBe('');
-
await nextTick();
expect(findReplyForm().exists()).toBe(false);
});
@@ -295,15 +289,15 @@ describe('Design discussions component', () => {
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
'applies correct class to all notes in the active discussion',
(note) => {
- createComponent(
- { discussion: mockDiscussion },
- {
+ createComponent({
+ props: { discussion: mockDiscussion },
+ data: {
activeDiscussion: {
id: note.id,
source: 'pin',
},
},
- );
+ });
expect(
wrapper
@@ -329,10 +323,10 @@ describe('Design discussions component', () => {
});
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -359,15 +353,15 @@ describe('Design discussions component', () => {
beforeEach(() => {
window.gon = { current_user_id: null };
- createComponent(
- {
+ createComponent({
+ props: {
discussion: {
...defaultMockDiscussion,
},
discussionWithOpenForm: defaultMockDiscussion.id,
},
- { discussionComment: 'test', isFormRendered: true },
- );
+ data: { isFormRendered: true },
+ });
});
it('does not render resolve discussion button', () => {
@@ -378,10 +372,6 @@ describe('Design discussions component', () => {
expect(findReplyPlaceholder().exists()).toBe(false);
});
- it('does not render apollo-mutation component', () => {
- expect(findApolloMutation().exists()).toBe(false);
- });
-
it('renders design-note-signed-out component', () => {
expect(findDesignNoteSignedOut().exists()).toBe(true);
expect(findDesignNoteSignedOut().props()).toMatchObject({
@@ -390,4 +380,64 @@ describe('Design discussions component', () => {
});
});
});
+
+ it('should open confirmation modal when the note emits `delete-note` event', async () => {
+ createComponent();
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: '1' });
+ expect(confirmAction).toHaveBeenCalled();
+ });
+
+ describe('when confirmation modal is opened', () => {
+ const noteId = 'note-test-id';
+
+ it('sends the mutation with correct variables', async () => {
+ confirmAction.mockResolvedValueOnce(true);
+ const destroyNoteMutationSuccess = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote', errors: [] } },
+ });
+ createComponent({ apolloConfig: { mutate: destroyNoteMutationSuccess } });
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId });
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(destroyNoteMutationSuccess).toHaveBeenCalledWith({
+ update: expect.any(Function),
+ mutation: destroyNoteMutation,
+ variables: {
+ input: {
+ id: noteId,
+ },
+ },
+ optimisticResponse: {
+ destroyNote: {
+ note: null,
+ errors: [],
+ __typename: 'DestroyNotePayload',
+ },
+ },
+ });
+ });
+
+ it('emits `delete-note-error` event if GraphQL mutation fails', async () => {
+ confirmAction.mockResolvedValueOnce(true);
+ const destroyNoteMutationError = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ createComponent({ apolloConfig: { mutate: destroyNoteMutationError } });
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId });
+
+ await waitForPromises();
+
+ expect(destroyNoteMutationError).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted()).toEqual({
+ 'delete-note-error': [[DELETE_NOTE_ERROR_MSG]],
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
index e71bb5ab520..95b08b89809 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
@@ -18,10 +18,6 @@ function createComponent(isAddDiscussion = false) {
describe('DesignNoteSignedOut', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders message containing register and sign-in links while user wants to reply to a discussion', () => {
wrapper = createComponent();
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 df511586c10..82848bd1a19 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
@@ -1,6 +1,6 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
-import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
@@ -38,6 +38,8 @@ describe('Design note component', () => {
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findEditButton = () => wrapper.findByTestId('note-edit');
const findNoteContent = () => wrapper.findByTestId('note-text');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-button"]');
function createComponent(props = {}, data = { isEditing: false }) {
wrapper = shallowMountExtended(DesignNote, {
@@ -63,10 +65,6 @@ describe('Design note component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should match the snapshot', () => {
createComponent({
note,
@@ -112,6 +110,14 @@ describe('Design note component', () => {
expect(findEditButton().exists()).toBe(false);
});
+ it('should not display a dropdown if user does not have a permission to delete note', () => {
+ createComponent({
+ note,
+ });
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
describe('when user has a permission to edit note', () => {
it('should open an edit form on edit button click', async () => {
createComponent({
@@ -158,15 +164,47 @@ describe('Design note component', () => {
expect(findNoteContent().exists()).toBe(true);
});
- it('calls a mutation on submit-form event and hides a form', async () => {
- findReplyForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalled();
+ it('hides a form after update mutation is completed', async () => {
+ findReplyForm().vm.$emit('note-submit-complete', { data: { updateNote: { errors: [] } } });
- await mutate();
await nextTick();
expect(findReplyForm().exists()).toBe(false);
expect(findNoteContent().exists()).toBe(true);
});
});
});
+
+ describe('when user has a permission to delete note', () => {
+ it('should display a dropdown', () => {
+ createComponent({
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ it('should emit `delete-note` event with proper payload when delete note button is clicked', async () => {
+ const payload = {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ };
+
+ createComponent({
+ note: {
+ ...payload,
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
+ });
});
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 f4d4f9cf896..db1cfb4f504 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,46 +1,96 @@
+import { GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Autosave from '~/autosave';
+import waitForPromises from 'helpers/wait_for_promises';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import {
+ ADD_DISCUSSION_COMMENT_ERROR,
+ ADD_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_NOTE_ERROR,
+} from '~/design_management/utils/error_messages';
+import {
+ mockNoteSubmitSuccessMutationResponse,
+ mockNoteSubmitFailureMutationResponse,
+} from '../../mock_data/apollo_mock';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/autosave');
describe('Design reply form component', () => {
let wrapper;
- let originalGon;
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
-
- function createComponent(props = {}, mountOptions = {}) {
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const mockNoteableId = 'gid://gitlab/DesignManagement::Design/6';
+ const mockComment = 'New comment';
+ const mockDiscussionId = 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8';
+ const createNoteMutationData = {
+ mutation: createNoteMutation,
+ update: expect.anything(),
+ variables: {
+ input: {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ body: mockComment,
+ },
+ },
+ };
+
+ const ctrlKey = {
+ ctrlKey: true,
+ };
+ const metaKey = {
+ metaKey: true,
+ };
+ const mutationHandler = jest.fn().mockResolvedValue();
+
+ function createComponent({
+ props = {},
+ mountOptions = {},
+ data = {},
+ mutation = mutationHandler,
+ } = {}) {
wrapper = mount(DesignReplyForm, {
propsData: {
+ designNoteMutation: createNoteMutation,
+ noteableId: mockNoteableId,
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
value: '',
- isSaving: false,
- noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
...mountOptions,
+ mocks: {
+ $apollo: {
+ mutate: mutation,
+ },
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
});
}
beforeEach(() => {
- originalGon = window.gon;
window.gon.current_user_id = 1;
});
afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
confirmAction.mockReset();
});
it('textarea has focus after component mount', () => {
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
- createComponent({}, { attachTo: document.body });
+ createComponent({ mountOptions: { attachTo: document.body } });
expect(findTextarea().element).toEqual(document.activeElement);
});
@@ -64,7 +114,7 @@ describe('Design reply form component', () => {
});
it('renders button text as "Save comment" when creating a comment', () => {
- createComponent({ isNewComment: false });
+ createComponent({ props: { isNewComment: false } });
expect(findSubmitButton().html()).toMatchSnapshot();
});
@@ -76,7 +126,7 @@ describe('Design reply form component', () => {
`(
'initializes autosave support on discussion with proper key',
async ({ discussionId, shortDiscussionId }) => {
- createComponent({ discussionId });
+ createComponent({ props: { discussionId } });
await nextTick();
expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
@@ -88,32 +138,24 @@ describe('Design reply form component', () => {
);
describe('when form has no text', () => {
- beforeEach(() => {
- createComponent({
- value: '',
- });
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
});
it('submit button is disabled', () => {
expect(findSubmitButton().attributes().disabled).toBe('disabled');
});
- it('does not emit submitForm event on textarea ctrl+enter keydown', async () => {
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
- });
-
- await nextTick();
- expect(wrapper.emitted('submit-form')).toBeUndefined();
- });
-
- it('does not emit submitForm event on textarea meta+enter keydown', async () => {
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
- });
+ it.each`
+ key | keyData
+ ${'ctrl'} | ${ctrlKey}
+ ${'meta'} | ${metaKey}
+ `('does not perform mutation on textarea $key+enter keydown', async ({ keyData }) => {
+ findTextarea().trigger('keydown.enter', keyData);
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeUndefined();
+ expect(mutationHandler).not.toHaveBeenCalled();
});
it('emits cancelForm event on pressing escape button on textarea', () => {
@@ -129,118 +171,159 @@ describe('Design reply form component', () => {
});
});
- describe('when form has text', () => {
- beforeEach(() => {
- createComponent({
- value: 'test',
- });
- });
-
+ describe('when the form has text', () => {
it('submit button is enabled', () => {
+ createComponent({ props: { value: mockComment } });
expect(findSubmitButton().attributes().disabled).toBeUndefined();
});
- it('emits submitForm event on Comment button click', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ it('calls a mutation on submit button click event', async () => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
+ const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
+ mutation: successfulMutation,
+ });
findSubmitButton().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
- });
+ expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData);
- it('emits submitForm event on textarea ctrl+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ await waitForPromises();
+ expect(wrapper.emitted('note-submit-complete')).toEqual([
+ [mockNoteSubmitSuccessMutationResponse],
+ ]);
+ });
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
+ it.each`
+ key | keyData
+ ${'ctrl'} | ${ctrlKey}
+ ${'meta'} | ${metaKey}
+ `('does perform mutation on textarea $key+enter keydown', async ({ keyData }) => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
+ const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
+ mutation: successfulMutation,
});
+ findTextarea().trigger('keydown.enter', keyData);
+
await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
- });
+ expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData);
- it('emits submitForm event on textarea meta+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ await waitForPromises();
+ expect(wrapper.emitted('note-submit-complete')).toEqual([
+ [mockNoteSubmitSuccessMutationResponse],
+ ]);
+ });
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
+ it('shows error message when mutation fails', async () => {
+ const failedMutation = jest.fn().mockRejectedValue(mockNoteSubmitFailureMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ value: mockComment,
+ },
+ mutation: failedMutation,
+ data: {
+ errorMessage: 'error',
+ },
});
- await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
- });
-
- it('emits input event on changing textarea content', async () => {
- findTextarea().setValue('test2');
+ findSubmitButton().vm.$emit('click');
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['test2']]);
+ await waitForPromises();
+ expect(findAlert().exists()).toBe(true);
});
+ it.each`
+ isDiscussion | isNewComment | errorMessage
+ ${true} | ${true} | ${ADD_IMAGE_DIFF_NOTE_ERROR}
+ ${true} | ${false} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR}
+ ${false} | ${true} | ${ADD_DISCUSSION_COMMENT_ERROR}
+ ${false} | ${false} | ${UPDATE_NOTE_ERROR}
+ `(
+ 'return proper error message on error in case of isDiscussion is $isDiscussion and isNewComment is $isNewComment',
+ async ({ isDiscussion, isNewComment, errorMessage }) => {
+ createComponent({ props: { isDiscussion, isNewComment } });
+
+ expect(wrapper.vm.getErrorMessage()).toBe(errorMessage);
+ },
+ );
+
it('emits cancelForm event on Escape key if text was not changed', () => {
+ createComponent();
+
findTextarea().trigger('keyup.esc');
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
it('opens confirmation modal on Escape key when text has changed', async () => {
- wrapper.setProps({ value: 'test2' });
+ createComponent();
+
+ findTextarea().setValue(mockComment);
await nextTick();
findTextarea().trigger('keyup.esc');
- expect(confirmAction).toHaveBeenCalled();
- });
-
- it('emits cancelForm event on Cancel button click if text was not changed', () => {
- findCancelButton().trigger('click');
- expect(wrapper.emitted('cancel-form')).toHaveLength(1);
- });
-
- it('opens confirmation modal on Cancel button click when text has changed', async () => {
- wrapper.setProps({ value: 'test2' });
-
- await nextTick();
- findCancelButton().trigger('click');
expect(confirmAction).toHaveBeenCalled();
});
it('emits cancelForm event when confirmed', async () => {
confirmAction.mockResolvedValueOnce(true);
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
- wrapper.setProps({ value: 'test3' });
- await nextTick();
+ createComponent({ props: { value: mockComment } });
+ findTextarea().setValue('Comment changed');
- findTextarea().trigger('keyup.esc');
await nextTick();
+ findTextarea().trigger('keyup.esc');
expect(confirmAction).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
});
- it("doesn't emit cancelForm event when not confirmed", async () => {
+ it('does not emit cancelForm event when not confirmed', async () => {
confirmAction.mockResolvedValueOnce(false);
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
- wrapper.setProps({ value: 'test3' });
+ createComponent({ props: { value: mockComment } });
+ findTextarea().setValue('Comment changed');
await nextTick();
findTextarea().trigger('keyup.esc');
await nextTick();
expect(confirmAction).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('cancel-form')).toBeUndefined();
- expect(autosaveResetSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when component is destroyed', () => {
+ it('calls autosave.reset', async () => {
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ createComponent();
+ await wrapper.destroy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
index 41129e2b58d..eaa5a620fa6 100644
--- a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
+++ b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
@@ -23,10 +23,6 @@ describe('Toggle replies widget component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when replies are collapsed', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index 4a339899473..fdcea6d88c0 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -15,7 +15,6 @@ const mockOverlayData = {
};
describe('Design management design presentation component', () => {
- const originalGon = window.gon;
let wrapper;
function createComponent(
@@ -114,11 +113,6 @@ describe('Design management design presentation component', () => {
window.gon = { current_user_id: 1 };
});
- afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
- });
-
it('renders image and overlay when image provided', async () => {
createComponent(
{
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index e1a66cea329..62a26a8f5dd 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -25,11 +25,6 @@ describe('Design management design scaler component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when `scale` value is greater than 1', () => {
beforeEach(async () => {
setScale(1.6);
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index af995f75ddc..90424175417 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -29,7 +29,6 @@ const $route = {
const mutate = jest.fn().mockResolvedValue();
describe('Design management design sidebar component', () => {
- const originalGon = window.gon;
let wrapper;
const findDiscussions = () => wrapper.findAllComponents(DesignDiscussion);
@@ -67,11 +66,6 @@ describe('Design management design sidebar component', () => {
window.gon = { current_user_id: 1 };
});
- afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
- });
-
it('renders participants', () => {
createComponent();
@@ -143,8 +137,8 @@ describe('Design management design sidebar component', () => {
expect(findResolvedCommentsToggle().props('visible')).toBe(true);
});
- it('sends a mutation to set an active discussion when clicking on a discussion', () => {
- findFirstDiscussion().trigger('click');
+ it('emits correct event to send a mutation to set an active discussion when clicking on a discussion', () => {
+ findFirstDiscussion().vm.$emit('update-active-discussion');
expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
});
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index ac26873b692..f713203c0ee 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -51,8 +51,6 @@ describe('Design management design todo button', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
jest.clearAllMocks();
});
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 95d2ad504de..53abcc559d8 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -20,10 +20,6 @@ describe('Design management large image component', () => {
stubPerformanceWebAPI();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders loading state', () => {
createComponent({
isLoading: true,
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index e907e2e4ac5..4a0ad5a045b 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -54,10 +54,6 @@ describe('Design management list item component', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when item is not in view', () => {
it('image is not rendered', () => {
createComponent();
diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 38a7fadee79..8427d83ceee 100644
--- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -34,10 +34,6 @@ describe('Design management pagination component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('hides components when designs are empty', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 1776405ece9..764ad73805f 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -1,12 +1,18 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import DeleteButton from '~/design_management/components/delete_button.vue';
import Toolbar from '~/design_management/components/toolbar/index.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+import { getPermissionsQueryResponse } from '../../mock_data/apollo_mock';
Vue.use(VueRouter);
+Vue.use(VueApollo);
const router = new VueRouter();
const RouterLinkStub = {
@@ -27,7 +33,12 @@ describe('Design management toolbar component', () => {
const updatedAt = new Date();
updatedAt.setHours(updatedAt.getHours() - 1);
+ const mockApollo = createMockApollo([
+ [permissionsQuery, jest.fn().mockResolvedValue(getPermissionsQueryResponse(createDesign))],
+ ]);
+
wrapper = shallowMount(Toolbar, {
+ apolloProvider: mockApollo,
router,
propsData: {
id: '1',
@@ -46,31 +57,20 @@ describe('Design management toolbar component', () => {
'router-link': RouterLinkStub,
},
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- permissions: {
- createDesign,
- },
- });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders design and updated data', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.element).toMatchSnapshot();
});
it('links back to designs list', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
const link = wrapper.find('a');
expect(link.props('to')).toEqual({
@@ -84,35 +84,41 @@ describe('Design management toolbar component', () => {
it('renders delete button on latest designs version with logged in user', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.findComponent(DeleteButton).exists()).toBe(true);
});
it('does not render delete button on non-latest version', async () => {
createComponent(false, true, { isLatestVersion: false });
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('does not render delete button when user is not logged in', async () => {
createComponent(false, false);
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs');
expect(wrapper.emitted().delete).toHaveLength(1);
});
- it('renders download button with correct link', () => {
+ it('renders download button with correct link', async () => {
createComponent();
+ await waitForPromises();
+
expect(wrapper.findComponent(GlButton).attributes('href')).toBe(
'/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
);
diff --git a/spec/frontend/design_management/components/upload/button_spec.js b/spec/frontend/design_management/components/upload/button_spec.js
index 59821218ab8..ceae7920e0d 100644
--- a/spec/frontend/design_management/components/upload/button_spec.js
+++ b/spec/frontend/design_management/components/upload/button_spec.js
@@ -14,10 +14,6 @@ describe('Design management upload button component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders upload design button', () => {
createComponent();
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index 6ad10e707ab..cdfff61ba4f 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -42,10 +42,6 @@ describe('Design management design version dropdown component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 2a43b5debee..2b99dcf14da 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -91,7 +91,7 @@ export const designUploadMutationUpdatedResponse = {
},
};
-export const permissionsQueryResponse = {
+export const getPermissionsQueryResponse = (createDesign = true) => ({
data: {
project: {
__typename: 'Project',
@@ -99,11 +99,11 @@ export const permissionsQueryResponse = {
issue: {
__typename: 'Issue',
id: 'issue-1',
- userPermissions: { __typename: 'UserPermissions', createDesign: true },
+ userPermissions: { __typename: 'UserPermissions', createDesign },
},
},
},
-};
+});
export const reorderedDesigns = [
{
@@ -211,3 +211,109 @@ export const getDesignQueryResponse = {
},
},
};
+
+export const mockNoteSubmitSuccessMutationResponse = [
+ {
+ data: {
+ createNote: {
+ note: {
+ id: 'gid://gitlab/DiffNote/468',
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ body: 'New comment',
+ bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
+ createdAt: '2023-02-24T06:49:20Z',
+ resolved: false,
+ position: {
+ diffRefs: {
+ baseSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
+ startSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
+ headSha: 'f348c652f1a737151fc79047895e695fbe81464c',
+ __typename: 'DiffRefs',
+ },
+ x: 441,
+ y: 128,
+ height: 152,
+ width: 695,
+ __typename: 'DiffPosition',
+ },
+ userPermissions: {
+ adminNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiffNote/459',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ errors: [],
+ __typename: 'CreateNotePayload',
+ },
+ },
+ },
+];
+
+export const mockNoteSubmitFailureMutationResponse = [
+ {
+ errors: [
+ {
+ message:
+ 'Variable $input of type CreateNoteInput! was provided invalid value for bodyaa (Field is not defined on CreateNoteInput), body (Expected value to not be null)',
+ locations: [
+ {
+ line: 1,
+ column: 21,
+ },
+ ],
+ extensions: {
+ value: {
+ noteableId: 'gid://gitlab/DesignManagement::Design/10',
+ discussionId: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8',
+ bodyaa: 'df',
+ },
+ problems: [
+ {
+ path: ['bodyaa'],
+ explanation: 'Field is not defined on CreateNoteInput',
+ },
+ {
+ path: ['body'],
+ explanation: 'Expected value to not be null',
+ },
+ ],
+ },
+ },
+ ],
+ },
+];
+
+export const mockCreateImageNoteDiffResponse = {
+ data: {
+ createImageDiffNote: {
+ note: {
+ author: {
+ username: '',
+ },
+ discussion: {},
+ },
+ },
+ },
+};
diff --git a/spec/frontend/design_management/mock_data/project.js b/spec/frontend/design_management/mock_data/project.js
new file mode 100644
index 00000000000..e1c2057d8d1
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/project.js
@@ -0,0 +1,17 @@
+import design from './design';
+
+export default {
+ project: {
+ issue: {
+ designCollection: {
+ designs: {
+ nodes: [
+ {
+ ...design,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index a11463ab663..6cec4036d40 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -1,15 +1,14 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-import { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import DesignPresentation from '~/design_management/components/design_presentation.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
-import createImageDiffNoteMutation from '~/design_management/graphql/mutations/create_image_diff_note.mutation.graphql';
import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import getDesignQuery from '~/design_management/graphql/queries/get_design.query.graphql';
import DesignIndex from '~/design_management/pages/design/index.vue';
import createRouter from '~/design_management/router';
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
@@ -23,16 +22,23 @@ import {
DESIGN_SNOWPLOW_EVENT_TYPES,
DESIGN_SERVICE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
+import * as cacheUpdate from '~/design_management/utils/cache_update';
import mockAllVersions from '../../mock_data/all_versions';
import design from '../../mock_data/design';
+import mockProject from '../../mock_data/project';
import mockResponseWithDesigns from '../../mock_data/designs';
import mockResponseNoDesigns from '../../mock_data/no_designs';
+import { mockCreateImageNoteDiffResponse } from '../../mock_data/apollo_mock';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/api.js');
const focusInput = jest.fn();
+const mockCacheObject = {
+ readQuery: jest.fn().mockReturnValue(mockProject),
+ writeQuery: jest.fn(),
+};
const mutate = jest.fn().mockResolvedValue();
const mockPageLayoutElement = {
classList: {
@@ -52,32 +58,13 @@ const mockDesignNoDiscussions = {
nodes: [],
},
};
-const newComment = 'new comment';
+
const annotationCoordinates = {
x: 10,
y: 10,
width: 100,
height: 100,
};
-const createDiscussionMutationVariables = {
- mutation: createImageDiffNoteMutation,
- update: expect.anything(),
- variables: {
- input: {
- body: newComment,
- noteableId: design.id,
- position: {
- headSha: 'headSha',
- baseSha: 'baseSha',
- startSha: 'startSha',
- paths: {
- newPath: 'full-design-path',
- },
- ...annotationCoordinates,
- },
- },
- },
-};
Vue.use(VueRouter);
@@ -85,7 +72,7 @@ describe('Design management design index page', () => {
let wrapper;
let router;
- const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm);
+ const findDesignReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findSidebar = () => wrapper.findComponent(DesignSidebar);
const findDesignPresentation = () => wrapper.findComponent(DesignPresentation);
@@ -95,7 +82,7 @@ describe('Design management design index page', () => {
data = {},
intialRouteOptions = {},
provide = {},
- stubs = { ApolloMutation, DesignSidebar, DesignReplyForm },
+ stubs = { DesignSidebar, DesignReplyForm },
} = {},
) {
const $apollo = {
@@ -105,6 +92,11 @@ describe('Design management design index page', () => {
},
},
mutate,
+ getClient() {
+ return {
+ cache: mockCacheObject,
+ };
+ },
};
router = createRouter();
@@ -133,10 +125,6 @@ describe('Design management design index page', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when navigating to component', () => {
it('applies fullscreen layout class', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
@@ -216,7 +204,7 @@ describe('Design management design index page', () => {
findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 });
await nextTick();
- expect(findDiscussionForm().exists()).toBe(true);
+ expect(findDesignReplyForm().exists()).toBe(true);
});
it('keeps new discussion form focused', () => {
@@ -235,24 +223,36 @@ describe('Design management design index page', () => {
expect(focusInput).toHaveBeenCalled();
});
- it('sends a mutation on submitting form and closes form', async () => {
+ it('sends a update and closes the form when mutation is completed', async () => {
createComponent(
{ loading: false },
{
data: {
design,
annotationCoordinates,
- comment: newComment,
},
},
);
- findDiscussionForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
+ const addImageDiffNoteToStore = jest.spyOn(cacheUpdate, 'updateStoreAfterAddImageDiffNote');
+
+ const mockDesignVariables = {
+ fullPath: 'project-path',
+ iid: '1',
+ filenames: ['gid::/gitlab/Design/1'],
+ atVersion: null,
+ };
+
+ findDesignReplyForm().vm.$emit('note-submit-complete', mockCreateImageNoteDiffResponse);
await nextTick();
- await mutate({ variables: createDiscussionMutationVariables });
- expect(findDiscussionForm().exists()).toBe(false);
+ expect(addImageDiffNoteToStore).toHaveBeenCalledWith(
+ mockCacheObject,
+ mockCreateImageNoteDiffResponse.data.createImageDiffNote,
+ getDesignQuery,
+ mockDesignVariables,
+ );
+ expect(findDesignReplyForm().exists()).toBe(false);
});
it('closes the form and clears the comment on canceling form', async () => {
@@ -262,17 +262,14 @@ describe('Design management design index page', () => {
data: {
design,
annotationCoordinates,
- comment: newComment,
},
},
);
- findDiscussionForm().vm.$emit('cancel-form');
-
- expect(wrapper.vm.comment).toBe('');
+ findDesignReplyForm().vm.$emit('cancel-form');
await nextTick();
- expect(findDiscussionForm().exists()).toBe(false);
+ expect(findDesignReplyForm().exists()).toBe(false);
});
describe('with error', () => {
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 76ece922ded..1ddf757eb19 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -29,19 +29,19 @@ import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
} from '~/design_management/utils/tracking';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
designListQueryResponse,
designUploadMutationCreatedResponse,
designUploadMutationUpdatedResponse,
- permissionsQueryResponse,
+ getPermissionsQueryResponse,
moveDesignMutationResponse,
reorderedDesigns,
moveDesignMutationResponseWithErrors,
} from '../mock_data/apollo_mock';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
const mockPageEl = {
classList: {
remove: jest.fn(),
@@ -181,7 +181,7 @@ describe('Design management index page', () => {
const requestHandlers = [
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
- [permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
+ [permissionsQuery, jest.fn().mockResolvedValue(getPermissionsQueryResponse())],
[moveDesignMutation, moveDesignHandler],
];
@@ -197,11 +197,6 @@ describe('Design management index page', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('designs', () => {
it('renders loading icon', () => {
createComponent({ loading: true });
@@ -800,7 +795,7 @@ describe('Design management index page', () => {
expect(draggableAttributes().disabled).toBe(false);
});
- it('displays flash if mutation had a recoverable error', async () => {
+ it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
});
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index b9edde559c8..3503725f741 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -11,8 +11,6 @@ import '~/commons/bootstrap';
function factory(routeArg) {
Vue.use(VueRouter);
- window.gon = { sprite_icons: '' };
-
const router = createRouter('/');
if (routeArg !== undefined) {
router.push(routeArg);
@@ -36,10 +34,6 @@ function factory(routeArg) {
}
describe('Design management router', () => {
- afterEach(() => {
- window.location.hash = '';
- });
-
describe.each([['/'], [{ name: DESIGNS_ROUTE_NAME }]])('root route', (routeArg) => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 42777adfd58..e89dfe9f860 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -10,10 +10,10 @@ import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import design from '../mock_data/design';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Design Management cache update', () => {
const mockErrors = ['code red!'];
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 513e67ea247..06995706a2b 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -59,6 +59,7 @@ describe('diffs/components/app', () => {
endpoint: TEST_ENDPOINT,
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
+ endpointDiffForPath: TEST_ENDPOINT,
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
projectPath: 'namespace/project',
@@ -71,12 +72,6 @@ describe('diffs/components/app', () => {
},
provide,
store,
- stubs: {
- DynamicScroller: {
- template: `<div><slot :item="$store.state.diffs.diffFiles[0]"></slot></div>`,
- },
- DynamicScrollerItem: true,
- },
});
}
@@ -265,7 +260,7 @@ describe('diffs/components/app', () => {
it('sets width of tree list', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
+ state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
});
expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px');
@@ -294,13 +289,14 @@ describe('diffs/components/app', () => {
it('does not render empty state when diff files exist', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({
- id: 1,
- });
+ state.diffs.diffFiles = ['anything'];
+ state.diffs.treeEntries['1'] = { type: 'blob', id: 1 };
});
expect(wrapper.findComponent(NoChanges).exists()).toBe(false);
- expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')).toBe(
+ store.state.diffs.diffFiles,
+ );
});
});
@@ -388,19 +384,15 @@ describe('diffs/components/app', () => {
beforeEach(() => {
createComponent({}, () => {
- store.state.diffs.diffFiles = [
- { file_hash: '111', file_path: '111.js' },
- { file_hash: '222', file_path: '222.js' },
- { file_hash: '333', file_path: '333.js' },
+ store.state.diffs.treeEntries = [
+ { type: 'blob', fileHash: '111', path: '111.js' },
+ { type: 'blob', fileHash: '222', path: '222.js' },
+ { type: 'blob', fileHash: '333', path: '333.js' },
];
});
spy = jest.spyOn(store, 'dispatch');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('jumps to next and previous files in the list', async () => {
await nextTick();
@@ -507,7 +499,6 @@ describe('diffs/components/app', () => {
describe('diffs', () => {
it('should render compare versions component', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
state.diffs.mergeRequestDiffs = diffsMockData;
state.diffs.targetBranchName = 'target-branch';
state.diffs.mergeRequestDiff = mergeRequestDiff;
@@ -578,10 +569,18 @@ describe('diffs/components/app', () => {
it('should display diff file if there are diff files', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
+ state.diffs.treeEntries = {
+ 111: { type: 'blob', fileHash: '111', path: '111.js' },
+ 123: { type: 'blob', fileHash: '123', path: '123.js' },
+ 312: { type: 'blob', fileHash: '312', path: '312.js' },
+ };
});
- expect(wrapper.findComponent(DiffFile).exists()).toBe(true);
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')).toBe(
+ store.state.diffs.diffFiles,
+ );
});
it("doesn't render tree list when no changes exist", () => {
@@ -592,7 +591,7 @@ describe('diffs/components/app', () => {
it('should render tree list', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
+ state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
});
expect(wrapper.findComponent(TreeList).exists()).toBe(true);
@@ -606,7 +605,7 @@ describe('diffs/components/app', () => {
it('calls setShowTreeList when only 1 file', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.treeEntries = { 123: { type: 'blob', fileHash: '123' } };
});
jest.spyOn(store, 'dispatch');
wrapper.vm.setTreeDisplay();
@@ -617,10 +616,12 @@ describe('diffs/components/app', () => {
});
});
- it('calls setShowTreeList with true when more than 1 file is in diffs array', () => {
+ it('calls setShowTreeList with true when more than 1 file is in tree entries map', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
- state.diffs.diffFiles.push({ sha: '124' });
+ state.diffs.treeEntries = {
+ 111: { type: 'blob', fileHash: '111', path: '111.js' },
+ 123: { type: 'blob', fileHash: '123', path: '123.js' },
+ };
});
jest.spyOn(store, 'dispatch');
@@ -640,7 +641,7 @@ describe('diffs/components/app', () => {
localStorage.setItem('mr_tree_show', showTreeList);
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.treeEntries['123'] = { sha: '123' };
});
jest.spyOn(store, 'dispatch');
@@ -656,7 +657,10 @@ describe('diffs/components/app', () => {
describe('file-by-file', () => {
it('renders a single diff', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
state.diffs.diffFiles.push({ file_hash: '312' });
});
@@ -671,7 +675,10 @@ describe('diffs/components/app', () => {
it('sets previous button as disabled', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
});
await nextTick();
@@ -682,7 +689,10 @@ describe('diffs/components/app', () => {
it('sets next button as disabled', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
state.diffs.currentDiffFileId = '312';
});
@@ -694,7 +704,7 @@ describe('diffs/components/app', () => {
it("doesn't display when there's fewer than 2 files", async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' });
+ state.diffs.treeEntries = { 123: { type: 'blob', fileHash: '123' } };
state.diffs.currentDiffFileId = '123';
});
@@ -711,7 +721,10 @@ describe('diffs/components/app', () => {
'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' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
state.diffs.currentDiffFileId = currentDiffFileId;
});
diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
index eca5b536a35..ae40f6c898d 100644
--- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -45,10 +45,6 @@ describe('CollapsedFilesWarning', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there is more than one file', () => {
it.each`
present | dismissed
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 08be3fa2745..4b4b6351d3f 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -41,11 +41,6 @@ describe('diffs/components/commit_item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default state', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
index 09128b04caa..785ff537777 100644
--- a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
+++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
@@ -38,11 +38,6 @@ describe('CompareDropdownLayout', () => {
isActive: listItem.classes().includes('is-active'),
}));
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with versions', () => {
beforeEach(() => {
const versions = [
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 21f3ee26bf8..23da1a3601b 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -58,11 +58,6 @@ describe('CompareVersions', () => {
store.state.diffs.mergeRequestDiffs = diffsMockData;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
beforeEach(() => {
createWrapper({}, {}, false);
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index 7bd9afab648..e5ca90eb7c8 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -11,10 +11,6 @@ const findIcon = () => wrapper.findComponent(GlIcon);
const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
describe('DiffCodeQuality', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
const createWrapper = (codeQuality, mountFunction = mountExtended) => {
return mountFunction(DiffCodeQuality, {
propsData: {
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 0bce6451ce4..3524973278c 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -93,11 +93,6 @@ describe('DiffContent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with text based files', () => {
afterEach(() => {
[isParallelViewGetterMock, isInlineViewGetterMock].forEach((m) => m.mockRestore());
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
index bf4a1a1c1f7..348439d6006 100644
--- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -26,10 +26,6 @@ describe('DiffDiscussionReply', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('if user can reply', () => {
beforeEach(() => {
getters = {
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 5092ae6ab6e..73d9f2d6d45 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -25,10 +25,6 @@ describe('DiffDiscussions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('should have notes list', () => {
createComponent();
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index c23eb2f3d24..4515a8e8926 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -72,8 +72,6 @@ describe('DiffFileHeader component', () => {
diffHasExpandedDiscussionsResultMock,
...Object.values(mockStoreConfig.modules.diffs.actions),
].forEach((mock) => mock.mockReset());
-
- wrapper.destroy();
});
const findHeader = () => wrapper.findComponent({ ref: 'header' });
diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js
index c5b76551fcc..66ee4e955b8 100644
--- a/spec/frontend/diffs/components/diff_file_row_spec.js
+++ b/spec/frontend/diffs/components/diff_file_row_spec.js
@@ -13,10 +13,6 @@ describe('Diff File Row component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders file row component', () => {
const sharedProps = {
level: 4,
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index ccfc36f8f16..93698396450 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -129,8 +129,6 @@ describe('DiffFile', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
axiosMock.restore();
});
@@ -222,21 +220,10 @@ describe('DiffFile', () => {
describe('computed', () => {
describe('showLocalFileReviews', () => {
- let gon;
-
function setLoggedIn(bool) {
window.gon.current_user_id = bool;
}
- beforeAll(() => {
- gon = window.gon;
- window.gon = {};
- });
-
- afterEach(() => {
- window.gon = gon;
- });
-
it.each`
loggedIn | bool
${true} | ${true}
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
index f13988fc11f..5f2b1a81b91 100644
--- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -21,10 +21,6 @@ describe('DiffGutterAvatars', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when expanded', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index a7a95ed2f35..356c7ef925a 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -89,10 +89,6 @@ describe('DiffRow', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
- window.gon = {};
showCommentForm.mockReset();
enterdragging.mockReset();
stopdragging.mockReset();
diff --git a/spec/frontend/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js
index bbd4f5faeec..d9359fb3c7b 100644
--- a/spec/frontend/diffs/components/hidden_files_warning_spec.js
+++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js
@@ -23,10 +23,6 @@ describe('HiddenFilesWarning', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a correct plain diff URL', () => {
const plainDiffLink = wrapper.findAllComponents(GlButton).at(0);
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index ccf942bdcef..18901781587 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -36,10 +36,6 @@ describe('Diffs image diff overlay component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders comment badges', () => {
createComponent();
diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
index 4e47249f5b4..715912b361f 100644
--- a/spec/frontend/diffs/components/merge_conflict_warning_spec.js
+++ b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
@@ -25,10 +25,6 @@ describe('MergeConflictWarning', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
present | resolutionPath
${false} | ${''}
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index dbfe9770e07..e637b1dd43d 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -34,11 +34,6 @@ describe('Diff no changes empty state', () => {
store.state.diffs.mergeRequestDiffs = diffsMockData;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findMessage = () => wrapper.find('[data-testid="no-changes-message"]');
it('prevents XSS', () => {
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index 2ec11ba86fd..3d2bbe43746 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -39,7 +39,6 @@ describe('Diff settings dropdown component', () => {
afterEach(() => {
store.dispatch.mockRestore();
- wrapper.destroy();
});
describe('tree view buttons', () => {
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 1656eaf8ba0..87c638d065a 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,20 +1,36 @@
-import { shallowMount, mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
-import FileTree from '~/vue_shared/components/file_tree.vue';
+import DiffFileRow from '~/diffs/components//diff_file_row.vue';
+import { stubComponent } from 'helpers/stub_component';
describe('Diffs tree list component', () => {
let wrapper;
let store;
- const getFileRows = () => wrapper.findAll('.file-row');
+ const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' });
+ const getFileRow = () => wrapper.findComponent(DiffFileRow);
Vue.use(Vuex);
- const createComponent = (mountFn = mount) => {
- wrapper = mountFn(TreeList, {
+ const createComponent = () => {
+ wrapper = shallowMount(TreeList, {
store,
propsData: { hideFileStats: false },
+ stubs: {
+ // eslint will fail if we import the real component
+ RecycleScroller: stubComponent(
+ {
+ name: 'RecycleScroller',
+ props: {
+ items: null,
+ },
+ },
+ {
+ template: '<div><slot :item="{ tree: [] }"></slot></div>',
+ },
+ ),
+ },
});
};
@@ -80,10 +96,6 @@ describe('Diffs tree list component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -101,26 +113,32 @@ describe('Diffs tree list component', () => {
});
describe('search by file extension', () => {
+ it('hides scroller for no matches', async () => {
+ wrapper.find('[data-testid="diff-tree-search"]').setValue('*.md');
+
+ await nextTick();
+
+ expect(getScroller().exists()).toBe(false);
+ expect(wrapper.text()).toContain('No files found');
+ });
+
it.each`
extension | itemSize
- ${'*.md'} | ${0}
- ${'*.js'} | ${1}
- ${'index.js'} | ${1}
- ${'app/*.js'} | ${1}
- ${'*.js, *.rb'} | ${2}
+ ${'*.js'} | ${2}
+ ${'index.js'} | ${2}
+ ${'app/*.js'} | ${2}
+ ${'*.js, *.rb'} | ${3}
`('returns $itemSize item for $extension', async ({ extension, itemSize }) => {
wrapper.find('[data-testid="diff-tree-search"]').setValue(extension);
await nextTick();
- expect(getFileRows()).toHaveLength(itemSize);
+ expect(getScroller().props('items')).toHaveLength(itemSize);
});
});
it('renders tree', () => {
- expect(getFileRows()).toHaveLength(2);
- expect(getFileRows().at(0).html()).toContain('index.js');
- expect(getFileRows().at(1).html()).toContain('app');
+ expect(getScroller().props('items')).toHaveLength(2);
});
it('hides file stats', async () => {
@@ -133,33 +151,16 @@ describe('Diffs tree list component', () => {
it('calls toggleTreeOpen when clicking folder', () => {
jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
- getFileRows().at(1).trigger('click');
+ getFileRow().vm.$emit('toggleTreeOpen', 'app');
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
});
- it('calls scrollToFile when clicking blob', () => {
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
-
- wrapper.find('.file-row').trigger('click');
-
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
- path: 'app/index.js',
- });
- });
-
- it('renders as file list when renderTreeList is false', async () => {
- wrapper.vm.$store.state.diffs.renderTreeList = false;
-
- await nextTick();
- expect(getFileRows()).toHaveLength(2);
- });
-
- it('renders file paths when renderTreeList is false', async () => {
+ it('renders when renderTreeList is false', async () => {
wrapper.vm.$store.state.diffs.renderTreeList = false;
await nextTick();
- expect(wrapper.find('.file-row').html()).toContain('index.js');
+ expect(getScroller().props('items')).toHaveLength(3);
});
});
@@ -172,12 +173,10 @@ describe('Diffs tree list component', () => {
});
it('passes the viewedDiffFileIds to the FileTree', async () => {
- createComponent(shallowMount);
+ createComponent();
await nextTick();
- // Have to use $attrs['viewed-files'] because we are passing down an object
- // and attributes('') stringifies values (e.g. [object])...
- expect(wrapper.findComponent(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
+ expect(wrapper.findComponent(DiffFileRow).props('viewedFiles')).toBe(viewedDiffFileIds);
});
});
});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 78765204322..b00076504e3 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -13,7 +13,7 @@ import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
@@ -26,7 +26,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('DiffsStoreActions', () => {
let mock;
@@ -69,6 +69,7 @@ describe('DiffsStoreActions', () => {
const endpoint = '/diffs/set/endpoint';
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
+ const endpointDiffForPath = '/diffs/set/endpoint/path';
const endpointCoverage = '/diffs/set/coverage_reports';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
@@ -83,6 +84,7 @@ describe('DiffsStoreActions', () => {
{
endpoint,
endpointBatch,
+ endpointDiffForPath,
endpointMetadata,
endpointCoverage,
projectPath,
@@ -93,6 +95,7 @@ describe('DiffsStoreActions', () => {
{
endpoint: '',
endpointBatch: '',
+ endpointDiffForPath: '',
endpointMetadata: '',
endpointCoverage: '',
projectPath: '',
@@ -106,6 +109,7 @@ describe('DiffsStoreActions', () => {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
projectPath,
dismissEndpoint,
@@ -236,13 +240,17 @@ describe('DiffsStoreActions', () => {
it('should show no warning on any other status code', async () => {
mock.onGet(endpointMetadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- await testAction(
- diffActions.fetchDiffFilesMeta,
- {},
- { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
- [{ type: types.SET_LOADING, payload: true }],
- [],
- );
+ try {
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+ } catch (error) {
+ expect(error.response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ }
expect(createAlert).not.toHaveBeenCalled();
});
@@ -265,7 +273,7 @@ describe('DiffsStoreActions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet(endpointCoverage).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
@@ -389,7 +397,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.assignDiscussionsToDiff,
[],
- { diffFiles: [] },
+ { diffFiles: [], flatBlobsList: [] },
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
);
@@ -1007,20 +1015,14 @@ describe('DiffsStoreActions', () => {
describe('setShowWhitespace', () => {
const endpointUpdateUser = 'user/prefs';
let putSpy;
- let gon;
beforeEach(() => {
putSpy = jest.spyOn(axios, 'put');
- gon = window.gon;
mock.onPut(endpointUpdateUser).reply(HTTP_STATUS_OK, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
});
- afterEach(() => {
- window.gon = gon;
- });
-
it('commits SET_SHOW_WHITESPACE', () => {
return testAction(
diffActions.setShowWhitespace,
@@ -1393,39 +1395,38 @@ describe('DiffsStoreActions', () => {
describe('setCurrentDiffFileIdFromNote', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
const commit = jest.fn();
- const state = { diffFiles: [{ file_hash: '123' }] };
+ const getters = { flatBlobsList: [{ fileHash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
notesById: { 1: { discussion_id: '2' } },
};
- diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, getters, rootGetters }, '1');
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, '123');
});
it('does not commit SET_CURRENT_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
- const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ id: '1' }),
notesById: { 1: { discussion_id: '2' } },
};
- diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
it('does not commit SET_CURRENT_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
- const state = { diffFiles: [{ file_hash: '123' }] };
+ const getters = { flatBlobsList: [{ fileHash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '124' } }),
notesById: { 1: { discussion_id: '2' } },
};
- diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, getters, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
@@ -1436,7 +1437,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.navigateToDiffFileIndex,
0,
- { diffFiles: [{ file_hash: '123' }] },
+ { flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
);
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 2e3a66d5b01..ed7b6699e2c 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -288,6 +288,19 @@ describe('Diffs Module Getters', () => {
});
});
+ describe('isTreePathLoaded', () => {
+ it.each`
+ desc | loaded | path | bool
+ ${'the file exists and has been loaded'} | ${true} | ${'path/tofile'} | ${true}
+ ${'the file exists and has not been loaded'} | ${false} | ${'path/tofile'} | ${false}
+ ${'the file does not exist'} | ${false} | ${'tofile/path'} | ${false}
+ `('returns $bool when $desc', ({ loaded, path, bool }) => {
+ localState.treeEntries['path/tofile'] = { diffLoaded: loaded };
+
+ expect(getters.isTreePathLoaded(localState)(path)).toBe(bool);
+ });
+ });
+
describe('allBlobs', () => {
it('returns an array of blobs', () => {
localState.treeEntries = {
@@ -328,7 +341,11 @@ describe('Diffs Module Getters', () => {
describe('currentDiffIndex', () => {
it('returns index of currently selected diff in diffList', () => {
- localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
+ localState.treeEntries = [
+ { type: 'blob', fileHash: '111' },
+ { type: 'blob', fileHash: '222' },
+ { type: 'blob', fileHash: '333' },
+ ];
localState.currentDiffFileId = '222';
expect(getters.currentDiffIndex(localState)).toEqual(1);
@@ -339,7 +356,11 @@ describe('Diffs Module Getters', () => {
});
it('returns 0 if no diff is selected yet or diff is not found', () => {
- localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
+ localState.treeEntries = [
+ { type: 'blob', fileHash: '111' },
+ { type: 'blob', fileHash: '222' },
+ { type: 'blob', fileHash: '333' },
+ ];
localState.currentDiffFileId = '';
expect(getters.currentDiffIndex(localState)).toEqual(0);
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index 031e4fe2be2..ed8d7397bbc 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -93,15 +93,20 @@ describe('DiffsStoreMutations', () => {
describe('SET_DIFF_DATA_BATCH_DATA', () => {
it('should set diff data batch type properly', () => {
- const state = { diffFiles: [] };
+ const mockFile = getDiffFileMock();
+ const state = {
+ diffFiles: [],
+ treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } },
+ };
const diffMock = {
- diff_files: [getDiffFileMock()],
+ diff_files: [mockFile],
};
mutations[types.SET_DIFF_DATA_BATCH](state, diffMock);
expect(state.diffFiles[0].renderIt).toEqual(true);
expect(state.diffFiles[0].collapsed).toEqual(false);
+ expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true);
});
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index b5c44b084d8..4760a8b7166 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -892,4 +892,61 @@ describe('DiffsStoreUtils', () => {
expect(files[6].right).toBeNull();
});
});
+
+ describe('isUrlHashNoteLink', () => {
+ it.each`
+ input | bool
+ ${'#note_12345'} | ${true}
+ ${'#12345'} | ${false}
+ ${'note_12345'} | ${true}
+ ${'12345'} | ${false}
+ `('returns $bool for $input', ({ bool, input }) => {
+ expect(utils.isUrlHashNoteLink(input)).toBe(bool);
+ });
+ });
+
+ describe('isUrlHashFileHeader', () => {
+ it.each`
+ input | bool
+ ${'#diff-content-12345'} | ${true}
+ ${'#12345'} | ${false}
+ ${'diff-content-12345'} | ${true}
+ ${'12345'} | ${false}
+ `('returns $bool for $input', ({ bool, input }) => {
+ expect(utils.isUrlHashFileHeader(input)).toBe(bool);
+ });
+ });
+
+ describe('parseUrlHashAsFileHash', () => {
+ it.each`
+ input | currentDiffId | resultId
+ ${'#note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
+ ${'note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
+ ${'#note_12345'} | ${undefined} | ${null}
+ ${'note_12345'} | ${undefined} | ${null}
+ ${'#diff-content-12345'} | ${undefined} | ${'12345'}
+ ${'diff-content-12345'} | ${undefined} | ${'12345'}
+ ${'#diff-content-12345'} | ${'98765'} | ${'12345'}
+ ${'diff-content-12345'} | ${'98765'} | ${'12345'}
+ ${'#e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'#Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ ${'Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ `('returns $resultId for $input and $currentDiffId', ({ input, currentDiffId, resultId }) => {
+ expect(utils.parseUrlHashAsFileHash(input, currentDiffId)).toBe(resultId);
+ });
+ });
+
+ describe('markTreeEntriesLoaded', () => {
+ it.each`
+ desc | entries | loaded | outcome
+ ${'marks an existing entry as loaded'} | ${{ abc: {} }} | ${[{ new_path: 'abc' }]} | ${{ abc: { diffLoaded: true } }}
+ ${'does nothing if the new file is not found in the tree entries'} | ${{ abc: {} }} | ${[{ new_path: 'def' }]} | ${{ abc: {} }}
+ ${'leaves entries unmodified if they are not in the loaded files'} | ${{ abc: {}, def: { diffLoaded: true }, ghi: {} }} | ${[{ new_path: 'ghi' }]} | ${{ abc: {}, def: { diffLoaded: true }, ghi: { diffLoaded: true } }}
+ `('$desc', ({ entries, loaded, outcome }) => {
+ expect(utils.markTreeEntriesLoaded({ priorEntries: entries, loadedFiles: loaded })).toEqual(
+ outcome,
+ );
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/tree_worker_utils_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
index 4df5fe75004..b8bd4fcd081 100644
--- a/spec/frontend/diffs/utils/tree_worker_utils_spec.js
+++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
@@ -75,8 +75,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 0,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'app/index.js',
+ old: undefined,
+ },
key: 'app/index.js',
name: 'index.js',
parentPath: 'app/',
@@ -97,8 +102,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 0,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'app/test/index.js',
+ old: undefined,
+ },
key: 'app/test/index.js',
name: 'index.js',
parentPath: 'app/test/',
@@ -112,8 +122,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 0,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'app/test/filepathneedstruncating.js',
+ old: undefined,
+ },
key: 'app/test/filepathneedstruncating.js',
name: 'filepathneedstruncating.js',
parentPath: 'app/test/',
@@ -138,8 +153,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 42,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'constructor/test/aFile.js',
+ old: undefined,
+ },
key: 'constructor/test/aFile.js',
name: 'aFile.js',
parentPath: 'constructor/test/',
@@ -160,10 +180,15 @@ describe('~/diffs/utils/tree_worker_utils', () => {
name: 'submodule @ abcdef123',
type: 'blob',
changed: true,
+ diffLoaded: false,
tempFile: true,
submodule: true,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'submodule @ abcdef123',
+ old: undefined,
+ },
addedLines: 1,
removedLines: 0,
tree: [],
@@ -175,10 +200,15 @@ describe('~/diffs/utils/tree_worker_utils', () => {
name: 'package.json',
type: 'blob',
changed: true,
+ diffLoaded: false,
tempFile: false,
submodule: undefined,
deleted: true,
fileHash: 'test',
+ filePaths: {
+ new: 'package.json',
+ old: undefined,
+ },
addedLines: 0,
removedLines: 0,
tree: [],
diff --git a/spec/frontend/drawio/content_editor_facade_spec.js b/spec/frontend/drawio/content_editor_facade_spec.js
new file mode 100644
index 00000000000..673968bac9f
--- /dev/null
+++ b/spec/frontend/drawio/content_editor_facade_spec.js
@@ -0,0 +1,138 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { create } from '~/drawio/content_editor_facade';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
+import axios from '~/lib/utils/axios_utils';
+import { PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML } from '../content_editor/test_constants';
+import { createTestEditor } from '../content_editor/test_utils';
+
+describe('drawio/contentEditorFacade', () => {
+ let tiptapEditor;
+ let axiosMock;
+ let contentEditorFacade;
+ let assetResolver;
+ const imageURL = '/group1/project1/-/wikis/test-file.drawio.svg';
+ const diagramSvg = '<svg></svg>';
+ const contentType = 'image/svg+xml';
+ const filename = 'test-file.drawio.svg';
+ const uploadsPath = '/uploads';
+ const canonicalSrc = '/new-diagram.drawio.svg';
+ const src = `/uploads${canonicalSrc}`;
+
+ beforeEach(() => {
+ assetResolver = {
+ resolveUrl: jest.fn(),
+ };
+ tiptapEditor = createTestEditor({ extensions: [DrawioDiagram] });
+ contentEditorFacade = create({
+ tiptapEditor,
+ drawioNodeName: DrawioDiagram.name,
+ uploadsPath,
+ assetResolver,
+ });
+ });
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ tiptapEditor.destroy();
+ });
+
+ describe('getDiagram', () => {
+ describe('when there is a selected diagram', () => {
+ beforeEach(() => {
+ tiptapEditor
+ .chain()
+ .setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML)
+ .setNodeSelection(1)
+ .run();
+ axiosMock
+ .onGet(imageURL)
+ .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType });
+ });
+
+ it('returns diagram information', async () => {
+ const diagram = await contentEditorFacade.getDiagram();
+
+ expect(diagram).toEqual({
+ diagramURL: imageURL,
+ filename,
+ diagramSvg,
+ contentType,
+ });
+ });
+ });
+
+ describe('when there is not a selected diagram', () => {
+ beforeEach(() => {
+ tiptapEditor.chain().setContent('<p>text</p>').setNodeSelection(1).run();
+ });
+
+ it('returns null', async () => {
+ const diagram = await contentEditorFacade.getDiagram();
+
+ expect(diagram).toBe(null);
+ });
+ });
+ });
+
+ describe('updateDiagram', () => {
+ beforeEach(() => {
+ tiptapEditor
+ .chain()
+ .setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML)
+ .setNodeSelection(1)
+ .run();
+
+ assetResolver.resolveUrl.mockReturnValueOnce(src);
+ contentEditorFacade.updateDiagram({ uploadResults: { file_path: canonicalSrc } });
+ });
+
+ it('updates selected diagram diagram node src and canonicalSrc', () => {
+ tiptapEditor.commands.setNodeSelection(1);
+ expect(tiptapEditor.state.selection.node.attrs).toMatchObject({
+ src,
+ canonicalSrc,
+ });
+ });
+ });
+
+ describe('insertDiagram', () => {
+ beforeEach(() => {
+ tiptapEditor.chain().setContent('<p></p>').run();
+
+ assetResolver.resolveUrl.mockReturnValueOnce(src);
+ contentEditorFacade.insertDiagram({ uploadResults: { file_path: canonicalSrc } });
+ });
+
+ it('inserts a new draw.io diagram in the document', () => {
+ tiptapEditor.commands.setNodeSelection(1);
+ expect(tiptapEditor.state.selection.node.attrs).toMatchObject({
+ src,
+ canonicalSrc,
+ });
+ });
+ });
+
+ describe('uploadDiagram', () => {
+ it('sends a post request to the uploadsPath containing the diagram svg', async () => {
+ const link = { markdown: '![](diagram.drawio.svg)' };
+ const blob = new Blob([diagramSvg], { type: 'image/svg+xml' });
+ const formData = new FormData();
+
+ formData.append('file', blob, filename);
+
+ axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, {
+ data: {
+ link,
+ },
+ });
+
+ const response = await contentEditorFacade.uploadDiagram({ diagramSvg, filename });
+
+ expect(response).not.toBe(link);
+ });
+ });
+});
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
new file mode 100644
index 00000000000..5ef26c04204
--- /dev/null
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -0,0 +1,479 @@
+import { launchDrawioEditor } from '~/drawio/drawio_editor';
+import {
+ DRAWIO_EDITOR_URL,
+ DRAWIO_FRAME_ID,
+ DIAGRAM_BACKGROUND_COLOR,
+ DRAWIO_IFRAME_TIMEOUT,
+ DIAGRAM_MAX_SIZE,
+} from '~/drawio/constants';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+jest.mock('~/alert');
+
+jest.useFakeTimers();
+
+describe('drawio/drawio_editor', () => {
+ let editorFacade;
+ let drawioIFrameReceivedMessages;
+ const diagramURL = `${window.location.origin}/uploads/diagram.drawio.svg`;
+ const testSvg = '<svg></svg>';
+ const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`;
+ const filename = 'diagram.drawio.svg';
+
+ const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID);
+ const waitForDrawioIFrameMessage = ({ messageNumber = 1 } = {}) =>
+ new Promise((resolve) => {
+ let messageCounter = 0;
+ const iframe = findDrawioIframe();
+
+ iframe?.contentWindow.addEventListener('message', (event) => {
+ drawioIFrameReceivedMessages.push(event);
+
+ messageCounter += 1;
+
+ if (messageCounter === messageNumber) {
+ resolve();
+ }
+ });
+ });
+ const expectDrawioIframeMessage = ({ expectation, messageNumber = 1 }) => {
+ expect(drawioIFrameReceivedMessages).toHaveLength(messageNumber);
+ expect(JSON.parse(drawioIFrameReceivedMessages[messageNumber - 1].data)).toEqual(expectation);
+ };
+ const postMessageToParentWindow = (data) => {
+ const event = new Event('message');
+
+ Object.setPrototypeOf(event, {
+ source: findDrawioIframe().contentWindow,
+ data: JSON.stringify(data),
+ });
+
+ window.dispatchEvent(event);
+ };
+
+ beforeEach(() => {
+ editorFacade = {
+ getDiagram: jest.fn(),
+ uploadDiagram: jest.fn(),
+ insertDiagram: jest.fn(),
+ updateDiagram: jest.fn(),
+ };
+ drawioIFrameReceivedMessages = [];
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ findDrawioIframe()?.remove();
+ });
+
+ describe('initializing', () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+ });
+
+ it('creates the drawio editor iframe and attaches it to the body', () => {
+ expect(findDrawioIframe().getAttribute('src')).toBe(DRAWIO_EDITOR_URL);
+ });
+
+ it('sets drawio-editor classname to the iframe', () => {
+ expect(findDrawioIframe().classList).toContain('drawio-editor');
+ });
+ });
+
+ describe(`when parent window does not receive configure event after ${DRAWIO_IFRAME_TIMEOUT} ms`, () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+ });
+
+ it('disposes draw.io iframe', () => {
+ expect(findDrawioIframe()).not.toBe(null);
+ jest.runAllTimers();
+ expect(findDrawioIframe()).toBe(null);
+ });
+
+ it('displays an alert indicating that the draw.io editor could not be loaded', () => {
+ jest.runAllTimers();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'The diagrams.net editor could not be loaded.',
+ });
+ });
+ });
+
+ describe('when parent window receives configure event', () => {
+ beforeEach(async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'configure' });
+
+ await waitForDrawioIFrameMessage();
+ });
+
+ it('sends configure action to the draw.io iframe', async () => {
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'configure',
+ config: {
+ darkColor: '#202020',
+ settingsName: 'gitlab',
+ },
+ colorSchemeMeta: false,
+ },
+ });
+ });
+
+ it('does not remove the iframe after the load error timeouts run', async () => {
+ jest.runAllTimers();
+
+ expect(findDrawioIframe()).not.toBe(null);
+ });
+ });
+
+ describe('when parent window receives init event', () => {
+ describe('when there isn’t a diagram selected', () => {
+ beforeEach(() => {
+ editorFacade.getDiagram.mockResolvedValueOnce(null);
+
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('sends load action to the draw.io iframe with empty svg and title', async () => {
+ await waitForDrawioIFrameMessage();
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'load',
+ xml: null,
+ border: 8,
+ background: DIAGRAM_BACKGROUND_COLOR,
+ dark: false,
+ title: null,
+ },
+ });
+ });
+ });
+
+ describe('when there is a diagram selected', () => {
+ const diagramSvg = '<svg></svg>';
+
+ beforeEach(() => {
+ editorFacade.getDiagram.mockResolvedValueOnce({
+ diagramURL,
+ diagramSvg,
+ filename,
+ contentType: 'image/svg+xml',
+ });
+
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('sends load action to the draw.io iframe with the selected diagram svg and filename', async () => {
+ await waitForDrawioIFrameMessage();
+
+ // Step 5: The draw.io editor will send the downloaded diagram to the iframe
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'load',
+ xml: diagramSvg,
+ border: 8,
+ background: DIAGRAM_BACKGROUND_COLOR,
+ dark: false,
+ title: filename,
+ },
+ });
+ });
+
+ it('sets the drawio iframe as visible and resets cursor', async () => {
+ await waitForDrawioIFrameMessage();
+
+ expect(findDrawioIframe().style.visibility).toBe('visible');
+ expect(findDrawioIframe().style.cursor).toBe('');
+ });
+
+ it('scrolls window to the top', async () => {
+ await waitForDrawioIFrameMessage();
+
+ expect(window.scrollX).toBe(0);
+ });
+ });
+
+ describe.each`
+ description | errorMessage | diagram
+ ${'when there is an image selected that is not an svg file'} | ${'The selected image is not a valid SVG diagram'} | ${{
+ diagramURL,
+ contentType: 'image/png',
+ filename: 'image.png',
+}}
+ ${'when the selected image is not an asset upload'} | ${'The selected image is not an asset uploaded in the application'} | ${{
+ diagramSvg: '<svg></svg>',
+ filename,
+ contentType: 'image/svg+xml',
+ diagramURL: 'https://example.com/image.drawio.svg',
+}}
+ ${'when the selected image is too large'} | ${'The selected image is too large.'} | ${{
+ diagramSvg: 'x'.repeat(DIAGRAM_MAX_SIZE + 1),
+ filename,
+ contentType: 'image/svg+xml',
+ diagramURL,
+}}
+ `('$description', ({ errorMessage, diagram }) => {
+ beforeEach(() => {
+ editorFacade.getDiagram.mockResolvedValueOnce(diagram);
+
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('displays an error alert indicating that the image is not a diagram', async () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: errorMessage,
+ error: expect.any(Error),
+ });
+ });
+
+ it('disposes the draw.io diagram iframe', () => {
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+
+ describe('when loading a diagram fails', () => {
+ beforeEach(() => {
+ editorFacade.getDiagram.mockRejectedValueOnce(new Error());
+
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('displays an error alert indicating the failure', async () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Cannot load the diagram into the diagrams.net editor',
+ error: expect.any(Error),
+ });
+ });
+
+ it('disposes the draw.io diagram iframe', () => {
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+ });
+
+ describe('when parent window receives prompt event', () => {
+ describe('when the filename is empty', () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'prompt', value: '' });
+ });
+
+ it('sends prompt action to the draw.io iframe requesting a filename', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 1 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'prompt',
+ titleKey: 'filename',
+ okKey: 'save',
+ defaultValue: 'diagram.drawio.svg',
+ },
+ messageNumber: 1,
+ });
+ });
+
+ it('sends dialog action to the draw.io iframe indicating that the filename cannot be empty', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 2 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'dialog',
+ titleKey: 'error',
+ messageKey: 'filenameShort',
+ buttonKey: 'ok',
+ },
+ messageNumber: 2,
+ });
+ });
+ });
+
+ describe('when the event data is not empty', () => {
+ beforeEach(async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'prompt', value: 'diagram.drawio.svg' });
+
+ await waitForDrawioIFrameMessage();
+ });
+
+ it('starts the saving file process', () => {
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'spinner',
+ show: true,
+ messageKey: 'saving',
+ },
+ });
+ });
+ });
+ });
+
+ describe('when parent receives export event', () => {
+ beforeEach(() => {
+ editorFacade.uploadDiagram.mockResolvedValueOnce({});
+ });
+
+ it('reloads diagram in the draw.io editor', async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage();
+
+ expectDrawioIframeMessage({
+ expectation: expect.objectContaining({
+ action: 'load',
+ xml: expect.stringContaining(testSvg),
+ }),
+ });
+ });
+
+ it('marks the diagram as modified in the draw.io editor', async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 2 });
+
+ expectDrawioIframeMessage({
+ expectation: expect.objectContaining({
+ action: 'status',
+ modified: true,
+ }),
+ messageNumber: 2,
+ });
+ });
+
+ describe('when the diagram filename is set', () => {
+ const TEST_FILENAME = 'diagram.drawio.svg';
+
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade, filename: TEST_FILENAME });
+ });
+
+ it('displays loading spinner in the draw.io editor', async () => {
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'spinner',
+ show: true,
+ messageKey: 'saving',
+ },
+ messageNumber: 3,
+ });
+ });
+
+ it('uploads exported diagram', async () => {
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expect(editorFacade.uploadDiagram).toHaveBeenCalledWith({
+ filename: TEST_FILENAME,
+ diagramSvg: expect.stringContaining(testSvg),
+ });
+ });
+
+ describe('when uploading the exported diagram succeeds', () => {
+ it('displays an alert indicating that the diagram was uploaded successfully', async () => {
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.any(String),
+ variant: VARIANT_SUCCESS,
+ fadeTransition: true,
+ });
+ });
+
+ it('disposes iframe', () => {
+ jest.runAllTimers();
+
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+
+ describe('when uploading the exported diagram fails', () => {
+ const uploadError = new Error();
+
+ beforeEach(() => {
+ editorFacade.uploadDiagram.mockReset();
+ editorFacade.uploadDiagram.mockRejectedValue(uploadError);
+
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+ });
+
+ it('hides loading indicator in the draw.io editor', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 4 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'spinner',
+ show: false,
+ },
+ messageNumber: 4,
+ });
+ });
+
+ it('displays an error dialog in the draw.io editor', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 5 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'dialog',
+ titleKey: 'error',
+ modified: true,
+ buttonKey: 'close',
+ messageKey: 'errorSavingFile',
+ },
+ messageNumber: 5,
+ });
+ });
+ });
+ });
+
+ describe('when diagram filename is not set', () => {
+ it('sends prompt action to the draw.io iframe', async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expect(drawioIFrameReceivedMessages[2].data).toEqual(
+ JSON.stringify({
+ action: 'prompt',
+ titleKey: 'filename',
+ okKey: 'save',
+ defaultValue: 'diagram.drawio.svg',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('when parent window receives exit event', () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+ });
+
+ it('disposes the the draw.io iframe', () => {
+ expect(findDrawioIframe()).not.toBe(null);
+
+ postMessageToParentWindow({ event: 'exit' });
+
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/drawio/markdown_field_editor_facade_spec.js b/spec/frontend/drawio/markdown_field_editor_facade_spec.js
new file mode 100644
index 00000000000..e3eafc63839
--- /dev/null
+++ b/spec/frontend/drawio/markdown_field_editor_facade_spec.js
@@ -0,0 +1,148 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { create } from '~/drawio/markdown_field_editor_facade';
+import * as textMarkdown from '~/lib/utils/text_markdown';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/lib/utils/text_markdown');
+
+describe('drawio/textareaMarkdownEditor', () => {
+ let textArea;
+ let textareaMarkdownEditor;
+ let axiosMock;
+
+ const markdownPreviewPath = '/markdown/preview';
+ const imageURL = '/assets/image.png';
+ const diagramMarkdown = '![](image.png)';
+ const diagramSvg = '<svg></svg>';
+ const contentType = 'image/svg+xml';
+ const filename = 'image.png';
+ const newDiagramMarkdown = '![](newdiagram.svg)';
+ const uploadsPath = '/uploads';
+
+ beforeEach(() => {
+ textArea = document.createElement('textarea');
+ textareaMarkdownEditor = create({ textArea, markdownPreviewPath, uploadsPath });
+
+ document.body.appendChild(textArea);
+ });
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ textArea.remove();
+ });
+
+ describe('getDiagram', () => {
+ describe('when there is a selected diagram', () => {
+ beforeEach(() => {
+ textMarkdown.resolveSelectedImage.mockReturnValueOnce({
+ imageURL,
+ imageMarkdown: diagramMarkdown,
+ filename,
+ });
+ axiosMock
+ .onGet(imageURL)
+ .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType });
+ });
+
+ it('returns diagram information', async () => {
+ const diagram = await textareaMarkdownEditor.getDiagram();
+
+ expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith(
+ textArea,
+ markdownPreviewPath,
+ );
+
+ expect(diagram).toEqual({
+ diagramURL: imageURL,
+ diagramMarkdown,
+ filename,
+ diagramSvg,
+ contentType,
+ });
+ });
+ });
+
+ describe('when there is not a selected diagram', () => {
+ beforeEach(() => {
+ textMarkdown.resolveSelectedImage.mockReturnValueOnce(null);
+ });
+
+ it('returns null', async () => {
+ const diagram = await textareaMarkdownEditor.getDiagram();
+
+ expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith(
+ textArea,
+ markdownPreviewPath,
+ );
+
+ expect(diagram).toBe(null);
+ });
+ });
+ });
+
+ describe('updateDiagram', () => {
+ beforeEach(() => {
+ jest.spyOn(textArea, 'focus');
+ jest.spyOn(textArea, 'dispatchEvent');
+
+ textArea.value = `diagram ${diagramMarkdown}`;
+
+ textareaMarkdownEditor.updateDiagram({
+ diagramMarkdown,
+ uploadResults: { link: { markdown: newDiagramMarkdown } },
+ });
+ });
+
+ it('focuses the textarea', () => {
+ expect(textArea.focus).toHaveBeenCalled();
+ });
+
+ it('replaces previous diagram markdown with new diagram markdown', () => {
+ expect(textArea.value).toBe(`diagram ${newDiagramMarkdown}`);
+ });
+
+ it('dispatches input event in the textarea', () => {
+ expect(textArea.dispatchEvent).toHaveBeenCalledWith(new Event('input'));
+ });
+ });
+
+ describe('insertDiagram', () => {
+ it('inserts markdown text and replaces any selected markdown in the textarea', () => {
+ textArea.value = `diagram ${diagramMarkdown}`;
+ textArea.setSelectionRange(0, 8);
+
+ textareaMarkdownEditor.insertDiagram({
+ uploadResults: { link: { markdown: newDiagramMarkdown } },
+ });
+
+ expect(textMarkdown.insertMarkdownText).toHaveBeenCalledWith({
+ textArea,
+ text: textArea.value,
+ tag: newDiagramMarkdown,
+ selected: textArea.value.substring(0, 8),
+ });
+ });
+ });
+
+ describe('uploadDiagram', () => {
+ it('sends a post request to the uploadsPath containing the diagram svg', async () => {
+ const link = { markdown: '![](diagram.drawio.svg)' };
+ const blob = new Blob([diagramSvg], { type: 'image/svg+xml' });
+ const formData = new FormData();
+
+ formData.append('file', blob, filename);
+
+ axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, {
+ link,
+ });
+
+ const response = await textareaMarkdownEditor.uploadDiagram({ diagramSvg, filename });
+
+ expect(response).toEqual({ link });
+ });
+ });
+});
diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js
index 12f90390c18..5cc66dd2ae0 100644
--- a/spec/frontend/editor/components/helpers.js
+++ b/spec/frontend/editor/components/helpers.js
@@ -1,4 +1,3 @@
-import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
@@ -9,7 +8,7 @@ export const buildButton = (id = 'foo-bar-btn', options = {}) => {
label: options.label || 'Foo Bar Button',
icon: options.icon || 'check',
selected: options.selected || false,
- group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP,
+ group: options.group,
onClick: options.onClick || (() => {}),
category: options.category || 'primary',
selectedLabel: options.selectedLabel || 'smth',
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index ff377494312..79692ab4557 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -21,11 +21,6 @@ describe('Source Editor Toolbar button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
const defaultProps = {
category: 'primary',
diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js
index bead39ca744..f737340a317 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js
@@ -5,7 +5,7 @@ import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
-import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
+import { EDITOR_TOOLBAR_BUTTON_GROUPS } from '~/editor/constants';
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
import { buildButton } from './helpers';
@@ -40,24 +40,24 @@ describe('Source Editor Toolbar', () => {
};
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
});
describe('groups', () => {
it.each`
- group | expectedGroup
- ${EDITOR_TOOLBAR_LEFT_GROUP} | ${EDITOR_TOOLBAR_LEFT_GROUP}
- ${EDITOR_TOOLBAR_RIGHT_GROUP} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
- ${undefined} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
- ${'non-existing'} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
+ group | expectedGroup
+ ${EDITOR_TOOLBAR_BUTTON_GROUPS.file} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.file}
+ ${EDITOR_TOOLBAR_BUTTON_GROUPS.edit} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.edit}
+ ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings}
+ ${undefined} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings}
+ ${'non-existing'} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings}
`('puts item with group="$group" into $expectedGroup group', ({ group, expectedGroup }) => {
const item = buildButton('first', {
group,
});
createComponentWithApollo([item]);
expect(findButtons()).toHaveLength(1);
- [EDITOR_TOOLBAR_RIGHT_GROUP, EDITOR_TOOLBAR_LEFT_GROUP].forEach((g) => {
+ Object.keys(EDITOR_TOOLBAR_BUTTON_GROUPS).forEach((g) => {
if (g === expectedGroup) {
expect(wrapper.vm.getGroupItems(g)).toEqual([expect.objectContaining({ id: 'first' })]);
} else {
@@ -70,7 +70,7 @@ describe('Source Editor Toolbar', () => {
describe('buttons update', () => {
it('properly updates buttons on Apollo cache update', async () => {
const item = buildButton('first', {
- group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.edit,
});
createComponentWithApollo();
@@ -95,12 +95,15 @@ describe('Source Editor Toolbar', () => {
describe('click handler', () => {
it('emits the "click" event when a button is clicked', () => {
const item1 = buildButton('first', {
- group: EDITOR_TOOLBAR_LEFT_GROUP,
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.file,
});
const item2 = buildButton('second', {
- group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.edit,
});
- createComponentWithApollo([item1, item2]);
+ const item3 = buildButton('third', {
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.settings,
+ });
+ createComponentWithApollo([item1, item2, item3]);
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
@@ -110,7 +113,10 @@ describe('Source Editor Toolbar', () => {
findButtons().at(1).vm.$emit('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2);
- expect(wrapper.vm.$emit.mock.calls).toHaveLength(2);
+ findButtons().at(2).vm.$emit('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item3);
+
+ expect(wrapper.vm.$emit.mock.calls).toHaveLength(3);
});
});
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index c822a0bfeaf..87208ec7aa8 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -33,6 +33,7 @@ import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml';
import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml';
import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
+import ServicesYaml from './yaml_tests/positive_tests/services.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
@@ -52,6 +53,7 @@ import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variabl
import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml';
import HooksNegative from './yaml_tests/negative_tests/hooks.yml';
import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml';
+import ServicesNegativeYaml from './yaml_tests/negative_tests/services.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -89,6 +91,7 @@ describe('positive tests', () => {
VariablesYaml,
ProjectPathYaml,
IdTokensYaml,
+ ServicesYaml,
SecretsYaml,
}),
)('schema validates %s', (_, input) => {
@@ -113,10 +116,12 @@ describe('negative tests', () => {
// YAML
ArtifactsNegativeYaml,
CacheKeyNeative,
+ HooksNegative,
IdTokensNegativeYaml,
IncludeNegativeYaml,
JobWhenNegativeYaml,
RulesNegativeYaml,
+ TriggerNegative,
VariablesInvalidOptionsYaml,
VariablesInvalidSyntaxDescYaml,
VariablesWrongSyntaxUsageExpand,
@@ -126,8 +131,7 @@ describe('negative tests', () => {
ProjectPathIncludeNoSlashYaml,
ProjectPathIncludeTailSlashYaml,
SecretsNegativeYaml,
- TriggerNegative,
- HooksNegative,
+ ServicesNegativeYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml
new file mode 100644
index 00000000000..6761a603a0a
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml
@@ -0,0 +1,38 @@
+empty_services:
+ services:
+
+without_name:
+ services:
+ - alias: db-postgres
+ entrypoint: ["/usr/local/bin/db-postgres"]
+ command: ["start"]
+
+invalid_entrypoint:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ entrypoint: "/usr/local/bin/db-postgres" # must be array
+
+empty_entrypoint:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ entrypoint: []
+
+invalid_command:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ command: "start" # must be array
+
+empty_command:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ command: []
+
+empty_pull_policy:
+ script: echo "Multiple pull policies."
+ services:
+ - name: postgres:11.6
+ pull_policy: []
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml
new file mode 100644
index 00000000000..8a0f59d1dfd
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml
@@ -0,0 +1,31 @@
+valid_services_list:
+ services:
+ - php:7
+ - node:latest
+ - golang:1.10
+
+valid_services_object:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ entrypoint: ["/usr/local/bin/db-postgres"]
+ command: ["start"]
+
+services_with_variables:
+ services:
+ - name: bitnami/nginx
+ alias: nginx
+ variables:
+ NGINX_HTTP_PORT_NUMBER: ${NGINX_HTTP_PORT_NUMBER}
+
+pull_policy_string:
+ script: echo "A single pull policy."
+ services:
+ - name: postgres:11.6
+ pull_policy: if-not-present
+
+pull_policy_array:
+ script: echo "Multiple pull policies."
+ services:
+ - name: postgres:11.6
+ pull_policy: [always, if-not-present]
diff --git a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
index 21f8979f1a9..e515285601b 100644
--- a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
@@ -17,7 +17,6 @@ describe('~/editor/editor_ci_config_ext', () => {
let editor;
let instance;
let editorEl;
- let originalGitlabUrl;
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
setHTMLFixture('<div id="editor"></div>');
@@ -31,16 +30,8 @@ describe('~/editor/editor_ci_config_ext', () => {
instance.use({ definition: CiSchemaExtension });
};
- beforeAll(() => {
- originalGitlabUrl = gon.gitlab_url;
- gon.gitlab_url = TEST_HOST;
- });
-
- afterAll(() => {
- gon.gitlab_url = originalGitlabUrl;
- });
-
beforeEach(() => {
+ gon.gitlab_url = TEST_HOST;
createMockEditor();
});
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index eab39ccaba1..b1b8173188c 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -7,6 +7,7 @@ import {
EDITOR_TYPE_DIFF,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS,
EXTENSION_BASE_LINE_NUMBERS_CLASS,
+ EXTENSION_SOFTWRAP_ID,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import EditorInstance from '~/editor/source_editor_instance';
@@ -35,8 +36,18 @@ describe('The basis for an Source Editor extension', () => {
},
};
};
- const createInstance = (baseInstance = {}) => {
- return new EditorInstance(baseInstance);
+ const baseInstance = {
+ getOption: jest.fn(),
+ };
+
+ const createInstance = (base = baseInstance) => {
+ return new EditorInstance(base);
+ };
+
+ const toolbar = {
+ addItems: jest.fn(),
+ updateItem: jest.fn(),
+ removeItems: jest.fn(),
};
beforeEach(() => {
@@ -49,6 +60,66 @@ describe('The basis for an Source Editor extension', () => {
resetHTMLFixture();
});
+ describe('onSetup callback', () => {
+ let instance;
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ });
+
+ it('adds correct buttons to the toolbar', () => {
+ instance.use({ definition: SourceEditorExtension });
+ expect(instance.toolbar.addItems).toHaveBeenCalledWith([
+ expect.objectContaining({
+ id: EXTENSION_SOFTWRAP_ID,
+ }),
+ ]);
+ });
+
+ it('does not fail if toolbar is not available', () => {
+ instance.toolbar = null;
+ expect(() => instance.use({ definition: SourceEditorExtension })).not.toThrow();
+ });
+
+ it.each`
+ optionValue | expectSelected
+ ${'on'} | ${true}
+ ${'off'} | ${false}
+ ${'foo'} | ${false}
+ ${undefined} | ${false}
+ ${null} | ${false}
+ `(
+ 'correctly sets the initial state of the button when wordWrap option is "$optionValue"',
+ ({ optionValue, expectSelected }) => {
+ instance.getOption.mockReturnValue(optionValue);
+ instance.use({ definition: SourceEditorExtension });
+ expect(instance.toolbar.addItems).toHaveBeenCalledWith([
+ expect.objectContaining({
+ selected: expectSelected,
+ }),
+ ]);
+ },
+ );
+ });
+
+ describe('onBeforeUnuse', () => {
+ let instance;
+ let extension;
+
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ extension = instance.use({ definition: SourceEditorExtension });
+ });
+ it('removes the registered buttons from the toolbar', () => {
+ expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(instance.toolbar.removeItems).toHaveBeenCalledWith([EXTENSION_SOFTWRAP_ID]);
+ });
+ });
+
describe('onUse callback', () => {
it('initializes the line highlighting', () => {
const instance = createInstance();
@@ -66,6 +137,7 @@ describe('The basis for an Source Editor extension', () => {
'$description the line linking for $instanceType instance',
({ instanceType, shouldBeCalled }) => {
const instance = createInstance({
+ ...baseInstance,
getEditorType: jest.fn().mockReturnValue(instanceType),
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
@@ -82,10 +154,44 @@ describe('The basis for an Source Editor extension', () => {
);
});
+ describe('toggleSoftwrap', () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ instance.use({ definition: SourceEditorExtension });
+ });
+
+ it.each`
+ currentWordWrap | newWordWrap | expectSelected
+ ${'on'} | ${'off'} | ${false}
+ ${'off'} | ${'on'} | ${true}
+ ${'foo'} | ${'on'} | ${true}
+ ${undefined} | ${'on'} | ${true}
+ ${null} | ${'on'} | ${true}
+ `(
+ 'correctly updates wordWrap option in editor and the state of the button when currentWordWrap is "$currentWordWrap"',
+ ({ currentWordWrap, newWordWrap, expectSelected }) => {
+ instance.getOption.mockReturnValue(currentWordWrap);
+ instance.updateOptions = jest.fn();
+ instance.toggleSoftwrap();
+ expect(instance.updateOptions).toHaveBeenCalledWith({
+ wordWrap: newWordWrap,
+ });
+ expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, {
+ selected: expectSelected,
+ });
+ },
+ );
+ });
+
describe('highlightLines', () => {
const revealSpy = jest.fn();
const decorationsSpy = jest.fn();
const instance = createInstance({
+ ...baseInstance,
revealLineInCenter: revealSpy,
deltaDecorations: decorationsSpy,
});
@@ -174,6 +280,7 @@ describe('The basis for an Source Editor extension', () => {
beforeEach(() => {
instance = createInstance({
+ ...baseInstance,
deltaDecorations: decorationsSpy,
lineDecorations,
});
@@ -188,6 +295,7 @@ describe('The basis for an Source Editor extension', () => {
describe('setupLineLinking', () => {
const instance = {
+ ...baseInstance,
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
deltaDecorations: jest.fn(),
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 33e4b4bfc8e..b226ef03b33 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -17,6 +17,7 @@ describe('Markdown Extension for Source Editor', () => {
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
const markdownPath = 'foo.md';
+ let extensions;
const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => {
const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
@@ -38,7 +39,10 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
- instance.use([{ definition: ToolbarExtension }, { definition: EditorMarkdownExtension }]);
+ extensions = instance.use([
+ { definition: ToolbarExtension },
+ { definition: EditorMarkdownExtension },
+ ]);
});
afterEach(() => {
@@ -59,6 +63,25 @@ describe('Markdown Extension for Source Editor', () => {
});
});
+ describe('markdown keystrokes', () => {
+ it('registers all keystrokes as actions', () => {
+ EXTENSION_MARKDOWN_BUTTONS.forEach((button) => {
+ if (button.data.mdShortcuts) {
+ expect(instance.getAction(button.id)).toBeDefined();
+ }
+ });
+ });
+
+ it('disposes all keystrokes on unuse', () => {
+ instance.unuse(extensions[1]);
+ EXTENSION_MARKDOWN_BUTTONS.forEach((button) => {
+ if (button.data.mdShortcuts) {
+ expect(instance.getAction(button.id)).toBeNull();
+ }
+ });
+ });
+ });
+
describe('getSelectedText', () => {
it('does not fail if there is no selection and returns the empty string', () => {
jest.spyOn(instance, 'getSelection');
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index c42ac28c498..895eb87833d 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -12,14 +12,14 @@ import {
} from '~/editor/constants';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import SourceEditor from '~/editor/source_editor';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import syntaxHighlight from '~/syntax_highlight';
import { spyOnApi } from './helpers';
jest.mock('~/syntax_highlight');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Markdown Live Preview Extension for Source Editor', () => {
let editor;
diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js
index f418eab668a..7e4079c17f7 100644
--- a/spec/frontend/editor/source_editor_webide_ext_spec.js
+++ b/spec/frontend/editor/source_editor_webide_ext_spec.js
@@ -13,7 +13,6 @@ describe('Source Editor Web IDE Extension', () => {
editorEl = document.getElementById('editor');
editor = new SourceEditor();
});
- afterEach(() => {});
describe('onSetup', () => {
it.each`
diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js
index e561cad1086..13b8a9804b0 100644
--- a/spec/frontend/editor/utils_spec.js
+++ b/spec/frontend/editor/utils_spec.js
@@ -54,19 +54,17 @@ describe('Source Editor utils', () => {
describe('getBlobLanguage', () => {
it.each`
- path | expectedLanguage
- ${'foo.js'} | ${'javascript'}
- ${'foo.js.rb'} | ${'ruby'}
- ${'foo.bar'} | ${'plaintext'}
- ${undefined} | ${'plaintext'}
- `(
- 'sets the $expectedThemeName theme when $themeName is set in the user preference',
- ({ path, expectedLanguage }) => {
- const language = utils.getBlobLanguage(path);
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.js.rb'} | ${'ruby'}
+ ${'foo.bar'} | ${'plaintext'}
+ ${undefined} | ${'plaintext'}
+ ${'foo/bar/foo.js'} | ${'javascript'}
+ `(`returns '$expectedLanguage' for '$path' path`, ({ path, expectedLanguage }) => {
+ const language = utils.getBlobLanguage(path);
- expect(language).toEqual(expectedLanguage);
- },
- );
+ expect(language).toEqual(expectedLanguage);
+ });
});
describe('setupCodeSnipet', () => {
diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js
index 90816f28d5b..272c1a09a69 100644
--- a/spec/frontend/emoji/components/category_spec.js
+++ b/spec/frontend/emoji/components/category_spec.js
@@ -9,11 +9,12 @@ function factory(propsData = {}) {
wrapper = shallowMount(Category, { propsData });
}
-describe('Emoji category component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
+const triggerGlIntersectionObserver = () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+ return nextTick();
+};
+describe('Emoji category component', () => {
beforeEach(() => {
factory({
category: 'Activity',
@@ -26,25 +27,19 @@ describe('Emoji category component', () => {
});
it('renders group', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ renderGroup: true });
+ await triggerGlIntersectionObserver();
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('renders group on appear', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
-
- await nextTick();
+ await triggerGlIntersectionObserver();
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('emits appear event on appear', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
-
- await nextTick();
+ await triggerGlIntersectionObserver();
expect(wrapper.emitted().appear[0]).toEqual(['Activity']);
});
diff --git a/spec/frontend/emoji/components/emoji_group_spec.js b/spec/frontend/emoji/components/emoji_group_spec.js
index 1aca2fbb8fc..75397ce25ff 100644
--- a/spec/frontend/emoji/components/emoji_group_spec.js
+++ b/spec/frontend/emoji/components/emoji_group_spec.js
@@ -15,10 +15,6 @@ function factory(propsData = {}) {
}
describe('Emoji group component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render any buttons', () => {
factory({
emojis: [],
diff --git a/spec/frontend/emoji/components/emoji_list_spec.js b/spec/frontend/emoji/components/emoji_list_spec.js
index a72ba614d9f..f6f6062f8e8 100644
--- a/spec/frontend/emoji/components/emoji_list_spec.js
+++ b/spec/frontend/emoji/components/emoji_list_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EmojiList from '~/emoji/components/emoji_list.vue';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji', () => ({
initEmojiMap: jest.fn(() => Promise.resolve()),
@@ -14,7 +14,8 @@ jest.mock('~/emoji', () => ({
}));
let wrapper;
-async function factory(render, propsData = { searchValue: '' }) {
+
+function factory(propsData = { searchValue: '' }) {
wrapper = extendedWrapper(
shallowMount(EmojiList, {
propsData,
@@ -23,35 +24,23 @@ async function factory(render, propsData = { searchValue: '' }) {
},
}),
);
-
- // Wait for categories to be set
- await nextTick();
-
- if (render) {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ render: true });
-
- // Wait for component to render
- await nextTick();
- }
}
const findDefaultSlot = () => wrapper.findByTestId('default-slot');
describe('Emoji list component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render until render is set', async () => {
- await factory(false);
+ factory();
expect(findDefaultSlot().exists()).toBe(false);
+ await waitForPromises();
+ expect(findDefaultSlot().exists()).toBe(true);
});
it('renders with none filtered list', async () => {
- await factory(true);
+ factory();
+
+ await waitForPromises();
expect(JSON.parse(findDefaultSlot().text())).toEqual({
activity: {
@@ -63,7 +52,9 @@ describe('Emoji list component', () => {
});
it('renders filtered list of emojis', async () => {
- await factory(true, { searchValue: 'smile' });
+ factory({ searchValue: 'smile' });
+
+ await waitForPromises();
expect(JSON.parse(findDefaultSlot().text())).toEqual({
search: {
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 82e3b50aeb8..7b160c48501 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -8,6 +8,7 @@ const {
setGlobalDateToRealDate,
} = require('./__helpers__/fake_date/fake_date');
const { TEST_HOST } = require('./__helpers__/test_constants');
+const { createGon } = require('./__helpers__/gon_helper');
const ROOT_PATH = path.resolve(__dirname, '../..');
@@ -40,11 +41,12 @@ class CustomEnvironment extends TestEnvironment {
});
const { IS_EE } = projectConfig.testEnvironmentOptions;
- this.global.gon = {
- ee: IS_EE,
- };
+
this.global.IS_EE = IS_EE;
+ // Set up global `gon` object
+ this.global.gon = createGon(IS_EE);
+
// Set up global `gl` object
this.global.gl = {};
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index 340740e6499..17ecd93361f 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -23,7 +23,7 @@ describe('/environments/components/canary_ingress.vue', () => {
...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
...options,
});
@@ -37,8 +37,6 @@ describe('/environments/components/canary_ingress.vue', () => {
if (wrapper) {
wrapper.destroy();
}
-
- wrapper = null;
});
describe('stable weight', () => {
diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js
index 31b1770da59..a101ed4e00a 100644
--- a/spec/frontend/environments/canary_update_modal_spec.js
+++ b/spec/frontend/environments/canary_update_modal_spec.js
@@ -34,8 +34,6 @@ describe('/environments/components/canary_update_modal.vue', () => {
if (wrapper) {
wrapper.destroy();
}
-
- wrapper = null;
});
beforeEach(() => {
@@ -47,7 +45,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
modalId: 'confirm-canary-change',
actionPrimary: {
text: 'Change ratio',
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
},
actionCancel: { text: 'Cancel' },
});
diff --git a/spec/frontend/environments/delete_environment_modal_spec.js b/spec/frontend/environments/delete_environment_modal_spec.js
index cc18bf754eb..96f6ce52a9c 100644
--- a/spec/frontend/environments/delete_environment_modal_spec.js
+++ b/spec/frontend/environments/delete_environment_modal_spec.js
@@ -6,10 +6,10 @@ import { s__, sprintf } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { resolvedEnvironment } from './graphql/mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
describe('~/environments/components/delete_environment_modal.vue', () => {
@@ -67,7 +67,7 @@ describe('~/environments/components/delete_environment_modal.vue', () => {
);
});
- it('should flash a message on error', async () => {
+ it('should alert a message on error', async () => {
createComponent({ apolloProvider: mockApollo });
deleteResolver.mockRejectedValue();
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index fb1a8b8c00a..34f338fabe6 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
@@ -37,7 +37,6 @@ describe('~/environments/components/edit.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const findNameInput = () => wrapper.findByLabelText('Name');
diff --git a/spec/frontend/environments/empty_state_spec.js b/spec/frontend/environments/empty_state_spec.js
index 02cf2dc3c68..d067c4c80e0 100644
--- a/spec/frontend/environments/empty_state_spec.js
+++ b/spec/frontend/environments/empty_state_spec.js
@@ -29,10 +29,6 @@ describe('~/environments/components/empty_state.vue', () => {
provide: { newEnvironmentPath: NEW_PATH },
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows an empty state for available environments', () => {
wrapper = createWrapper();
diff --git a/spec/frontend/environments/enable_review_app_modal_spec.js b/spec/frontend/environments/enable_review_app_modal_spec.js
index 7939bd600dc..ee728775980 100644
--- a/spec/frontend/environments/enable_review_app_modal_spec.js
+++ b/spec/frontend/environments/enable_review_app_modal_spec.js
@@ -18,10 +18,6 @@ describe('Enable Review App Modal', () => {
const findInstructionAt = (i) => wrapper.findAll('ol li').at(i);
const findCopyString = () => wrapper.find(`#${EXPECTED_COPY_PRE_ID}`);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders the modal', () => {
beforeEach(() => {
wrapper = extendedWrapper(
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 48483152f7a..3c9b4144e45 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -36,7 +36,7 @@ describe('EnvironmentActions Component', () => {
wrapper = mountFn(EnvironmentActions, {
propsData: { actions: [], ...props },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
...options,
});
@@ -52,7 +52,6 @@ describe('EnvironmentActions Component', () => {
};
afterEach(() => {
- wrapper.destroy();
confirmAction.mockReset();
});
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index a37515bc3f7..279ff32a13d 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => {
...propsData,
},
stubs: { transition: stubTransition() },
- provide: { helpPagePath: '/help' },
+ provide: { helpPagePath: '/help', projectId: '1' },
});
beforeEach(async () => {
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index b9b34bee80f..50e4e637aa3 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -15,19 +15,16 @@ const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings
describe('~/environments/components/form.vue', () => {
let wrapper;
- const createWrapper = (propsData = {}) =>
+ const createWrapper = (propsData = {}, options = {}) =>
mountExtended(EnvironmentForm, {
provide: PROVIDE,
+ ...options,
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
wrapper = createWrapper();
@@ -105,6 +102,7 @@ describe('~/environments/components/form.vue', () => {
wrapper = createWrapper({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+
describe('when a new environment is being created', () => {
beforeEach(() => {
wrapper = createWrapper({
@@ -133,6 +131,18 @@ describe('~/environments/components/form.vue', () => {
});
});
+ describe('when no protected environment link is provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ provide: {},
+ });
+ });
+
+ it('does not show protected environment documentation', () => {
+ expect(wrapper.findByRole('link', { name: 'Protected environments' }).exists()).toBe(false);
+ });
+ });
+
describe('when an existing environment is being edited', () => {
beforeEach(() => {
wrapper = createWrapper({
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index dd909cf4473..59e94dfd662 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -56,10 +56,6 @@ describe('Environment item', () => {
findUpcomingDeployment().findComponent(GlAvatarLink);
const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when item is not folder', () => {
it('should render environment name', () => {
expect(wrapper.find('.environment-name').text()).toContain(environment.name);
@@ -390,10 +386,6 @@ describe('Environment item', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render folder icon and name', () => {
expect(wrapper.find('.folder-name').text()).toContain(folder.name);
expect(wrapper.find('.folder-icon')).toBeDefined();
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 170036b5b00..2f38dea2833 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -31,10 +31,6 @@ describe('Pin Component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
@@ -64,10 +60,6 @@ describe('Pin Component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index a86cfdd56ba..652b0f807fe 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -34,10 +34,6 @@ describe('Environment table', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Should render a table', async () => {
const mockItem = {
name: 'review',
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 986ecca4e84..a843f801da5 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -96,10 +96,6 @@ describe('~/environments/components/environments_app.vue', () => {
paginationMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should request available environments if the scope is invalid', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 1f233c05fbf..8574743919f 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -47,7 +47,7 @@ describe('Environments detail header component', () => {
TimeAgo,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
canAdminEnvironment: false,
@@ -59,10 +59,6 @@ describe('Environments detail header component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default state with minimal access', () => {
beforeEach(() => {
createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index a87060f83d8..75fb3a31120 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -24,7 +24,6 @@ describe('Environments Folder View', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('successful request', () => {
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 5ea0be41614..b5435990042 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -798,3 +798,9 @@ export const resolvedDeploymentDetails = {
},
},
};
+
+export const agent = {
+ project: 'agent-project',
+ id: '1',
+ name: 'agent-name',
+};
diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js
new file mode 100644
index 00000000000..4a6e2a7373b
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_agent_info_spec.js
@@ -0,0 +1,126 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
+import { TOKEN_STATUS_ACTIVE } from '~/clusters/agents/constants';
+import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql';
+
+Vue.use(VueApollo);
+
+const propsData = {
+ agentName: 'my-agent',
+ agentId: '1',
+ agentProjectPath: 'path/to/agent-config-project',
+};
+
+const mockClusterAgent = {
+ id: '1',
+ name: 'token-1',
+ webPath: 'path/to/agent-page',
+};
+
+const connectedTimeNow = new Date();
+const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+
+describe('~/environments/components/kubernetes_agent_info.vue', () => {
+ let wrapper;
+ let agentQueryResponse;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAgentLink = () => wrapper.findComponent(GlLink);
+ const findAgentStatus = () => wrapper.findByTestId('agent-status');
+ const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon);
+ const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = ({ tokens = [], queryResponse = null } = {}) => {
+ const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } };
+
+ agentQueryResponse =
+ queryResponse ||
+ jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } });
+ const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]);
+
+ wrapper = extendedWrapper(
+ shallowMount(KubernetesAgentInfo, {
+ apolloProvider,
+ propsData,
+ stubs: { TimeAgoTooltip, GlSprintf },
+ }),
+ );
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows loading icon while fetching the agent details', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ await waitForPromises();
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('sends expected params', async () => {
+ await waitForPromises();
+
+ const variables = {
+ agentName: propsData.agentName,
+ projectPath: propsData.agentProjectPath,
+ tokenStatus: TOKEN_STATUS_ACTIVE,
+ };
+
+ expect(agentQueryResponse).toHaveBeenCalledWith(variables);
+ });
+
+ it('renders the agent name with the link', async () => {
+ await waitForPromises();
+
+ expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath);
+ expect(findAgentLink().text()).toContain(mockClusterAgent.id);
+ });
+ });
+
+ describe.each`
+ lastUsedAt | status | lastUsedText
+ ${null} | ${'unused'} | ${KubernetesAgentInfo.i18n.neverConnectedText}
+ ${connectedTimeNow} | ${'active'} | ${'just now'}
+ ${connectedTimeInactive} | ${'inactive'} | ${'8 minutes ago'}
+ `('when agent connection status is "$status"', ({ lastUsedAt, status, lastUsedText }) => {
+ beforeEach(async () => {
+ const tokens = [{ id: 'token-id', lastUsedAt }];
+ createWrapper({ tokens });
+ await waitForPromises();
+ });
+
+ it('displays correct status text', () => {
+ expect(findAgentStatus().text()).toBe(AGENT_STATUSES[status].name);
+ });
+
+ it('displays correct status icon', () => {
+ expect(findAgentStatusIcon().props('name')).toBe(AGENT_STATUSES[status].icon);
+ expect(findAgentStatusIcon().attributes('class')).toBe(AGENT_STATUSES[status].class);
+ });
+
+ it('displays correct last used date status', () => {
+ expect(findAgentLastUsedDate().text()).toBe(lastUsedText);
+ });
+ });
+
+ describe('when the agent query has errored', () => {
+ beforeEach(() => {
+ createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
+ return waitForPromises();
+ });
+
+ it('displays an alert message', () => {
+ expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
new file mode 100644
index 00000000000..8673c657760
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -0,0 +1,84 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlCollapse, GlButton } from '@gitlab/ui';
+import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
+
+const agent = {
+ project: 'agent-project',
+ id: '1',
+ name: 'agent-name',
+};
+
+const propsData = {
+ agentId: agent.id,
+ agentName: agent.name,
+ agentProjectPath: agent.project,
+};
+
+describe('~/environments/components/kubernetes_overview.vue', () => {
+ let wrapper;
+
+ const findCollapse = () => wrapper.findComponent(GlCollapse);
+ const findCollapseButton = () => wrapper.findComponent(GlButton);
+ const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
+
+ const createWrapper = () => {
+ wrapper = shallowMount(KubernetesOverview, {
+ propsData,
+ });
+ };
+
+ const toggleCollapse = async () => {
+ findCollapseButton().vm.$emit('click');
+ await nextTick();
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the kubernetes overview title', () => {
+ expect(wrapper.text()).toBe(KubernetesOverview.i18n.sectionTitle);
+ });
+ });
+
+ describe('collapse', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('is collapsed by default', () => {
+ expect(findCollapse().props('visible')).toBeUndefined();
+ expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.expand);
+ expect(findCollapseButton().props('icon')).toBe('chevron-right');
+ });
+
+ it("doesn't render components when the collapse is not visible", () => {
+ expect(findAgentInfo().exists()).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ findCollapseButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findCollapse().attributes('visible')).toBe('true');
+ expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.collapse);
+ expect(findCollapseButton().props('icon')).toBe('chevron-down');
+ });
+ });
+
+ describe('when section is expanded', () => {
+ it('renders kubernetes agent info', async () => {
+ createWrapper();
+ await toggleCollapse();
+
+ expect(findAgentInfo().props()).toEqual({
+ agentName: agent.name,
+ agentId: agent.id,
+ agentProjectPath: agent.project,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 76cd09cfb4e..c04ff896794 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -9,7 +9,8 @@ import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
-import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
+import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
Vue.use(VueApollo);
@@ -20,15 +21,16 @@ describe('~/environments/components/new_environment_item.vue', () => {
return createMockApollo();
};
- const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
+ const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
- provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' },
+ provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1', ...provideData },
stubs: { transition: stubTransition() },
});
const findDeployment = () => wrapper.findComponent(Deployment);
+ const findKubernetesOverview = () => wrapper.findComponent(KubernetesOverview);
const expandCollapsedSection = async () => {
const button = wrapper.findByRole('button', { name: __('Expand') });
@@ -37,10 +39,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
return button;
};
- afterEach(() => {
- wrapper?.destroy();
- });
-
it('displays the name when not in a folder', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
@@ -157,7 +155,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
describe('stop', () => {
- it('shows a buton to stop the environment if the environment is available', () => {
+ it('shows a button to stop the environment if the environment is available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
@@ -165,7 +163,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(stop.exists()).toBe(true);
});
- it('does not show a buton to stop the environment if the environment is stopped', () => {
+ it('does not show a button to stop the environment if the environment is stopped', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, canStop: false } },
apolloProvider: createApolloProvider(),
@@ -515,4 +513,71 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(deployBoard.exists()).toBe(false);
});
});
+
+ describe('kubernetes overview', () => {
+ const environmentWithAgent = {
+ ...resolvedEnvironment,
+ agent,
+ };
+
+ it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => {
+ wrapper = createWrapper({
+ propsData: { environment: environmentWithAgent },
+ provideData: {
+ glFeatures: {
+ kasUserAccessProject: true,
+ },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().props()).toMatchObject({
+ agentProjectPath: agent.project,
+ agentName: agent.name,
+ agentId: agent.id,
+ });
+ });
+
+ it('should not render if the feature flag is not enabled', () => {
+ wrapper = createWrapper({
+ propsData: { environment: environmentWithAgent },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+
+ it('should not render if the environment has no agent object', () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+
+ it('should not render if the environment has an agent object without agent id specified', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ agent: {
+ project: agent.project,
+ name: agent.name,
+ },
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index a8cc05b297b..743f4ad6786 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
@@ -41,7 +41,6 @@ describe('~/environments/components/new.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js
index a2ab4f707b5..ddf6670db12 100644
--- a/spec/frontend/environments/stop_stale_environments_modal_spec.js
+++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js
@@ -18,7 +18,6 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
let wrapper;
let mock;
let before;
- let originalGon;
const createWrapper = (opts = {}) =>
shallowMount(StopStaleEnvironmentsModal, {
@@ -28,8 +27,7 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
});
beforeEach(() => {
- originalGon = window.gon;
- window.gon = { api_version: 'v4' };
+ window.gon.api_version = 'v4';
mock = new MockAdapter(axios);
jest.spyOn(axios, 'post');
@@ -39,9 +37,7 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
jest.resetAllMocks();
- window.gon = originalGon;
});
it('sets the correct min and max dates', async () => {
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 9d6e46be8c4..3bfade12d27 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -18,11 +18,11 @@ import {
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '~/error_tracking/utils';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(Vuex);
@@ -148,7 +148,7 @@ describe('ErrorDetails', () => {
expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
});
- it('when timeout is hit and no apollo result, stops loading and shows flash', async () => {
+ it('when timeout is hit and no apollo result, stops loading and shows alert', async () => {
Date.now.mockReturnValue(endTime + 1);
wrapper.vm.onNoApolloResult();
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 3ec43010d80..44db4780ba9 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
let mock;
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 383d8aaeb20..0aeb8b19a9e 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
HTTP_STATUS_BAD_REQUEST,
@@ -14,7 +14,7 @@ import Poll from '~/lib/utils/poll';
let mockedAdapter;
let mockedRestart;
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('Sentry error details store actions', () => {
@@ -48,7 +48,7 @@ describe('Sentry error details store actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mockedAdapter.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 590983bd93d..24a26476455 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('error tracking actions', () => {
let mock;
@@ -38,7 +38,7 @@ describe('error tracking actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
diff --git a/spec/frontend/experimentation/components/gitlab_experiment_spec.js b/spec/frontend/experimentation/components/gitlab_experiment_spec.js
index f52ebf0f3c4..73db4b9503c 100644
--- a/spec/frontend/experimentation/components/gitlab_experiment_spec.js
+++ b/spec/frontend/experimentation/components/gitlab_experiment_spec.js
@@ -9,7 +9,6 @@ const defaultSlots = {
};
describe('ExperimentComponent', () => {
- const oldGon = window.gon;
let wrapper;
const createComponent = (propsData = defaultProps, slots = defaultSlots) => {
@@ -20,12 +19,6 @@ describe('ExperimentComponent', () => {
window.gon = { experiment: { experiment_name: { variant: expectedVariant } } };
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- window.gon = oldGon;
- });
-
describe('when variant and experiment is set', () => {
it('renders control when it is the active variant', () => {
mockVariant('control');
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index 0d663fd055e..6d9c9dfe65a 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -10,18 +10,15 @@ describe('experiment Utilities', () => {
const ABC_KEY = 'abc';
const DEF_KEY = 'def';
- let origGon;
let origGl;
beforeEach(() => {
- origGon = window.gon;
origGl = window.gl;
window.gon.experiment = {};
window.gl.experiments = {};
});
afterEach(() => {
- window.gon = origGon;
window.gl = origGl;
});
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index c1051a14a08..b06e0340991 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -42,7 +42,6 @@ describe('Configure Feature Flags Modal', () => {
wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'danger');
describe('idle', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory);
it('should have Primary and Secondary actions', () => {
@@ -51,7 +50,7 @@ describe('Configure Feature Flags Modal', () => {
});
it('should default disable the primary action', () => {
- const [{ disabled }] = findSecondaryAction().attributes;
+ const { disabled } = findSecondaryAction().attributes;
expect(disabled).toBe(true);
});
@@ -112,19 +111,17 @@ describe('Configure Feature Flags Modal', () => {
});
describe('verified', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory);
it('should enable the secondary action', async () => {
findProjectNameInput().vm.$emit('input', provide.projectName);
await nextTick();
- const [{ disabled }] = findSecondaryAction().attributes;
+ const { disabled } = findSecondaryAction().attributes;
expect(disabled).toBe(false);
});
});
describe('cannot rotate token', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory.bind(null, { canUserRotateToken: false }));
it('should not display the primary action', () => {
@@ -141,7 +138,6 @@ describe('Configure Feature Flags Modal', () => {
});
describe('has rotate error', () => {
- afterEach(() => wrapper.destroy());
beforeEach(() => {
factory({ hasRotateError: true });
});
@@ -153,7 +149,6 @@ describe('Configure Feature Flags Modal', () => {
});
describe('is rotating', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory.bind(null, { isRotating: true }));
it('should disable the project name input', async () => {
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index cf4605e21ea..c26fd80865d 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -53,7 +53,6 @@ describe('Edit feature flag form', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js
index e3cc6f703c4..d983332f7c1 100644
--- a/spec/frontend/feature_flags/components/empty_state_spec.js
+++ b/spec/frontend/feature_flags/components/empty_state_spec.js
@@ -48,8 +48,6 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
if (wrapper?.destroy) {
wrapper.destroy();
}
-
- wrapper = null;
});
describe('alerts', () => {
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index a4738fed37e..9fc0119a6c8 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -27,7 +27,6 @@ describe('Feature flags > Environments dropdown', () => {
const findDropdownMenu = () => wrapper.find('.dropdown-menu');
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index e80f9c559c4..23e86d0eb2f 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -65,8 +65,6 @@ describe('Feature flags', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
- wrapper = null;
});
describe('when limit exceeded', () => {
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index 7dd7c709c94..f66e25698e6 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -42,10 +42,6 @@ describe('feature flag form', () => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [] });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render provided submitText', () => {
factory(requiredProps);
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index 300d0e47082..46c9118cbd9 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -46,10 +46,6 @@ describe('New feature flag form', () => {
factory();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with error', () => {
it('should render the error', async () => {
store.dispatch('receiveCreateFeatureFlagError', { message: ['The name is required'] });
diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
index 70a9156b5a9..5feaf094701 100644
--- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
@@ -24,8 +24,6 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
if (wrapper?.destroy) {
wrapper.destroy();
}
-
- wrapper = null;
});
describe('with valid percentage', () => {
diff --git a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
index 23ad0d3a08d..365f1e534b5 100644
--- a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
@@ -28,8 +28,6 @@ describe('~/feature_flags/strategies/parameter_form_group.vue', () => {
if (wrapper?.destroy) {
wrapper.destroy();
}
-
- wrapper = null;
});
it('should display the default slot', () => {
diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
index cb422a018f9..b20061c12a2 100644
--- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
@@ -22,8 +22,6 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
if (wrapper?.destroy) {
wrapper.destroy();
}
-
- wrapper = null;
});
describe('with valid percentage', () => {
diff --git a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
index 0a72714c22a..ae489f3a6e6 100644
--- a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
@@ -22,8 +22,6 @@ describe('~/feature_flags/components/users_with_id.vue', () => {
if (wrapper?.destroy) {
wrapper.destroy();
}
-
- wrapper = null;
});
it('should display the current value of the parameters', () => {
diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
index d0f1f7d0e2a..cd8270f1801 100644
--- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
@@ -32,8 +32,6 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => {
if (wrapper?.destroy) {
wrapper.destroy();
}
-
- wrapper = null;
});
describe.each`
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index 4d5cb26810e..4609bfc23d7 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('feature highlight helper', () => {
describe('dismiss', () => {
@@ -26,7 +26,7 @@ describe('feature highlight helper', () => {
await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});
- it('triggers flash when dismiss request fails', async () => {
+ it('triggers an alert when dismiss request fails', async () => {
mockAxios
.onPost(endpoint, { feature_name: highlightId })
.replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
index 650f9eb1bbc..66ea22cece3 100644
--- a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
@@ -29,11 +29,6 @@ describe('feature_highlight/feature_highlight_popover', () => {
buildWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders popover target', () => {
expect(findPopoverTarget().exists()).toBe(true);
});
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 ebed477fa2f..5f0e928e1fe 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
@@ -22,11 +22,6 @@ describe('Recent Searches Dropdown Content', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when local storage is not available', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index 26f12673f68..02ef813883f 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -68,10 +68,6 @@ describe('Dropdown User', () => {
'/gitlab_directory/-/autocomplete/users.json',
);
});
-
- afterEach(() => {
- window.gon = {};
- });
});
describe('hideCurrentUser', () => {
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 26af7af701b..8c16ff100eb 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -8,11 +8,11 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
import { visitUrl, getParameterByName } from '~/lib/utils/url_utility';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
getParameterByName: jest.fn(),
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index d3fa8fae9ab..138a4e183a9 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -5,11 +5,11 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Filtered Search Visual Tokens', () => {
const findElements = (tokenElement) => {
diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb
index d8c8737b125..ad0fb9be8dc 100644
--- a/spec/frontend/fixtures/abuse_reports.rb
+++ b/spec/frontend/fixtures/abuse_reports.rb
@@ -14,6 +14,8 @@ RSpec.describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :co
render_views
before do
+ stub_feature_flags(abuse_reports_list: false)
+
sign_in(admin)
enable_admin_mode!(admin)
end
diff --git a/spec/frontend/fixtures/api_deploy_keys.rb b/spec/frontend/fixtures/api_deploy_keys.rb
index 5ffc726f086..8c926296817 100644
--- a/spec/frontend/fixtures/api_deploy_keys.rb
+++ b/spec/frontend/fixtures/api_deploy_keys.rb
@@ -7,6 +7,7 @@ RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let_it_be(:admin) { create(:admin) }
+ let_it_be(:path) { "/deploy_keys" }
let_it_be(:project) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:deploy_key) { create(:deploy_key, public: true) }
@@ -17,8 +18,10 @@ RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do
let_it_be(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key2) }
let_it_be(:deploy_keys_project4) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key2) }
+ it_behaves_like 'GET request permissions for admin mode'
+
it 'api/deploy_keys/index.json' do
- get api("/deploy_keys", admin)
+ get api("/deploy_keys", admin, admin_mode: true)
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 6d452bf1bff..3583beb83c2 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -93,4 +93,26 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
end
+
+ describe 'get_jobs_count.query.graphql', type: :request do
+ let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
+ let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) }
+ let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) }
+
+ fixtures_path = 'graphql/jobs/'
+ get_jobs_count_query = 'get_jobs_count.query.graphql'
+ full_path = 'frontend-fixtures/builds-project'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_count_query}")
+ end
+
+ it "#{fixtures_path}#{get_jobs_count_query}.json" do
+ post_graphql(query, current_user: user, variables: {
+ fullPath: full_path
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 7ee89ca3694..b6f6d149756 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -151,7 +151,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
context 'merge request with no approvals' do
base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
base_output_path = 'graphql/merge_requests/approvals/'
- query_name = 'approved_by.query.graphql'
+ query_name = 'approvals.query.graphql'
it "#{base_output_path}#{query_name}_no_approvals.json" do
query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
@@ -165,7 +165,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
context 'merge request approved by current user' do
base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
base_output_path = 'graphql/merge_requests/approvals/'
- query_name = 'approved_by.query.graphql'
+ query_name = 'approvals.query.graphql'
it "#{base_output_path}#{query_name}.json" do
merge_request.approved_by_users << user
@@ -181,7 +181,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
context 'merge request approved by multiple users' do
base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
base_output_path = 'graphql/merge_requests/approvals/'
- query_name = 'approved_by.query.graphql'
+ query_name = 'approvals.query.graphql'
it "#{base_output_path}#{query_name}_multiple_users.json" do
merge_request.approved_by_users << user
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index f60e4991292..1581bc58289 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -145,6 +145,40 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
end
+
+ describe 'runner_for_registration.query.graphql', :freeze_time, type: :request do
+ runner_for_registration_query = 'register/runner_for_registration.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{runner_for_registration_query}")
+ end
+
+ it "#{fixtures_path}#{runner_for_registration_query}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe 'runner_create.mutation.graphql', type: :request do
+ runner_create_mutation = 'new/runner_create.mutation.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{runner_create_mutation}")
+ end
+
+ it "#{fixtures_path}#{runner_create_mutation}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ input: {
+ description: 'My dummy runner'
+ }
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
describe 'as group owner', GraphQL::Query do
diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb
index c80ba06bca1..613e4a1b447 100644
--- a/spec/frontend/fixtures/saved_replies.rb
+++ b/spec/frontend/fixtures/saved_replies.rb
@@ -43,4 +43,32 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d
expect_graphql_errors_to_be_empty
end
end
+
+ context 'when user creates saved reply' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'create_saved_reply.mutation.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user, variables: { name: "Test", content: "Test content" })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user creates saved reply and it errors' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'create_saved_reply.mutation.graphql'
+
+ it "#{base_output_path}create_saved_reply_with_errors.mutation.graphql.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user, variables: { name: nil, content: nil })
+
+ expect(flattened_errors).not_to be_empty
+ end
+ end
end
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index bd2d63a1827..18a4aa58c00 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -16,7 +16,6 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
before do
# We want vNext badge to be included and com/canary don't remove/hide any other elements.
# This is why we're turning com and canary on by default for now.
- allow(Gitlab).to receive(:com?).and_return(true)
allow(Gitlab).to receive(:canary?).and_return(true)
sign_in(user)
end
@@ -55,13 +54,28 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful
end
+
+ # This Feature Flag is off by default
+ # This ensures that the correct css is generated for super sidebar
+ # When the feature flag is off, the general startup will capture it
+ it "startup_css/project-#{type}-super-sidebar.html" do
+ stub_feature_flags(super_sidebar_nav: true)
+ user.update!(use_new_navigation: true)
+
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ id: project
+ }
+
+ expect(response).to be_successful
+ end
end
- describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
+ describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
it_behaves_like 'startup css project fixtures', 'general'
end
- describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
+ describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
before do
user.update!(theme_id: 11)
end
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
deleted file mode 100644
index 96820c9ae80..00000000000
--- a/spec/frontend/fixtures/u2f.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.context 'U2F' do
- include JavaScriptFixturesHelpers
-
- let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') }
-
- before do
- stub_feature_flags(webauthn: false)
- end
-
- describe SessionsController, '(JavaScript fixtures)', type: :controller do
- include DeviseHelpers
-
- render_views
-
- before do
- set_devise_mapping(context: @request)
- end
-
- it 'u2f/authenticate.html' do
- allow(controller).to receive(:find_user).and_return(user)
-
- post :create, params: { user: { login: user.username, password: user.password } }
-
- expect(response).to be_successful
- end
- end
-
- describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do
- render_views
-
- before do
- sign_in(user)
- allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
- allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
- end
- end
-
- it 'u2f/register.html' do
- get :show
-
- expect(response).to be_successful
- end
- end
-end
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
new file mode 100644
index 00000000000..6271aa87b9a
--- /dev/null
+++ b/spec/frontend/fixtures/users.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Users (GraphQL fixtures)', feature_category: :user_profile do
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ context 'for user achievements' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement1) { create(:achievement, namespace: group) }
+ let_it_be(:achievement2) { create(:achievement, namespace: group) }
+ let_it_be(:achievement3) { create(:achievement, namespace: group) }
+ let_it_be(:achievement_with_avatar_and_description) do
+ create(:achievement,
+ namespace: group,
+ description: 'Description',
+ avatar: File.new(Rails.root.join('db/fixtures/development/rocket.jpg'), 'r'))
+ end
+
+ let(:user_achievements_query_path) { 'profile/components/graphql/get_user_achievements.query.graphql' }
+ let(:query) { get_graphql_query_as_string(user_achievements_query_path) }
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ it "graphql/get_user_achievements_empty_response.json" do
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "graphql/get_user_achievements_with_avatar_and_description_response.json" do
+ create(:user_achievement, user: user, achievement: achievement_with_avatar_and_description)
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "graphql/get_user_achievements_without_avatar_or_description_response.json" do
+ create(:user_achievement, user: user, achievement: achievement1)
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "graphql/get_user_achievements_long_response.json" do
+ [achievement1, achievement2, achievement3, achievement_with_avatar_and_description].each do |achievement|
+ create(:user_achievement, user: user, achievement: achievement)
+ end
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/webauthn.rb b/spec/frontend/fixtures/webauthn.rb
index c6e9b41b584..ed6180118f0 100644
--- a/spec/frontend/fixtures/webauthn.rb
+++ b/spec/frontend/fixtures/webauthn.rb
@@ -32,6 +32,7 @@ RSpec.context 'WebAuthn' do
allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
end
+ stub_feature_flags(webauthn_without_totp: false)
end
it 'webauthn/register.html' do
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index e1890555de0..4f5788dcb77 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -69,7 +69,6 @@ describe('Frequent Items App Component', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('default', () => {
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 c54a2a1d039..7c8592fdf0c 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
@@ -59,8 +59,6 @@ describe('FrequentItemsListItemComponent', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
- wrapper = null;
});
describe('computed', () => {
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 d024925f62b..87f8e131b77 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -29,10 +29,6 @@ describe('FrequentItemsListComponent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('isListEmpty', () => {
it('should return `true` or `false` representing whether if `items` is empty or not with projects', async () => {
diff --git a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
index 685b5144a95..b1adc3f794a 100644
--- a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
+++ b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
@@ -50,10 +50,6 @@ describe('PagesPipelineWizard', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the pipeline wizard', () => {
expect(findPipelineWizardWrapper().exists()).toBe(true);
});
diff --git a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
index 949bcf71ff5..e87f7e950cd 100644
--- a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
+++ b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
@@ -25,7 +25,6 @@ describe('GitlabVersionCheckBadge', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
diff --git a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
index 4809ea37045..a0c988830ed 100644
--- a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
+++ b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
@@ -15,10 +15,6 @@ describe('google_cloud/components/google_cloud_menu', () => {
wrapper = mountExtended(GoogleCloudMenu, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains active configuration link', () => {
const link = wrapper.findByTestId('configurationLink');
expect(link.text()).toBe(GoogleCloudMenu.i18n.configuration.title);
diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js
index 09a4d92dca2..92bc39bdff9 100644
--- a/spec/frontend/google_cloud/components/incubation_banner_spec.js
+++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js
@@ -15,10 +15,6 @@ describe('google_cloud/components/incubation_banner', () => {
wrapper = mount(IncubationBanner);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains alert', () => {
expect(findAlert().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/components/revoke_oauth_spec.js b/spec/frontend/google_cloud/components/revoke_oauth_spec.js
index faaec07fc35..2b39bb9ca74 100644
--- a/spec/frontend/google_cloud/components/revoke_oauth_spec.js
+++ b/spec/frontend/google_cloud/components/revoke_oauth_spec.js
@@ -20,10 +20,6 @@ describe('google_cloud/components/revoke_oauth', () => {
wrapper = shallowMount(RevokeOauth, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains title', () => {
const title = findTitle();
expect(title.text()).toContain('Revoke authorizations');
diff --git a/spec/frontend/google_cloud/configuration/panel_spec.js b/spec/frontend/google_cloud/configuration/panel_spec.js
index 79eb4cb4918..dd85b4c90a7 100644
--- a/spec/frontend/google_cloud/configuration/panel_spec.js
+++ b/spec/frontend/google_cloud/configuration/panel_spec.js
@@ -25,10 +25,6 @@ describe('google_cloud/configuration/panel', () => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
diff --git a/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
index 48e4b0ca1ad..6e2d3147a54 100644
--- a/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
+++ b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
@@ -25,10 +25,6 @@ describe('google_cloud/databases/cloudsql/create_instance_form', () => {
wrapper = shallowMountExtended(InstanceForm, { propsData, stubs: { GlFormCheckbox } });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
index a5736d0a524..a2ee75f9fbf 100644
--- a/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
+++ b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
@@ -8,10 +8,6 @@ describe('google_cloud/databases/cloudsql/instance_table', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTable = () => wrapper.findComponent(GlTable);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there are no instances', () => {
beforeEach(() => {
const propsData = {
diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js
index e6a0d74f348..779258bbdbb 100644
--- a/spec/frontend/google_cloud/databases/panel_spec.js
+++ b/spec/frontend/google_cloud/databases/panel_spec.js
@@ -23,10 +23,6 @@ describe('google_cloud/databases/panel', () => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
diff --git a/spec/frontend/google_cloud/databases/service_table_spec.js b/spec/frontend/google_cloud/databases/service_table_spec.js
index 4a622e544e1..4594e1758ad 100644
--- a/spec/frontend/google_cloud/databases/service_table_spec.js
+++ b/spec/frontend/google_cloud/databases/service_table_spec.js
@@ -19,10 +19,6 @@ describe('google_cloud/databases/service_table', () => {
wrapper = mountExtended(ServiceTable, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should contain a table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/deployments/panel_spec.js b/spec/frontend/google_cloud/deployments/panel_spec.js
index 729db1707a7..0748d8f9377 100644
--- a/spec/frontend/google_cloud/deployments/panel_spec.js
+++ b/spec/frontend/google_cloud/deployments/panel_spec.js
@@ -19,10 +19,6 @@ describe('google_cloud/deployments/panel', () => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
diff --git a/spec/frontend/google_cloud/deployments/service_table_spec.js b/spec/frontend/google_cloud/deployments/service_table_spec.js
index 8faad64e313..49220a6007e 100644
--- a/spec/frontend/google_cloud/deployments/service_table_spec.js
+++ b/spec/frontend/google_cloud/deployments/service_table_spec.js
@@ -18,10 +18,6 @@ describe('google_cloud/deployments/service_table', () => {
wrapper = mount(DeploymentsServiceTable, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should contain a table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/gcp_regions/form_spec.js b/spec/frontend/google_cloud/gcp_regions/form_spec.js
index 1030e9c8a18..be37ff092f0 100644
--- a/spec/frontend/google_cloud/gcp_regions/form_spec.js
+++ b/spec/frontend/google_cloud/gcp_regions/form_spec.js
@@ -16,10 +16,6 @@ describe('google_cloud/gcp_regions/form', () => {
wrapper = shallowMount(GcpRegionsForm, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/gcp_regions/list_spec.js b/spec/frontend/google_cloud/gcp_regions/list_spec.js
index 6d8c389e5a1..74a54b93183 100644
--- a/spec/frontend/google_cloud/gcp_regions/list_spec.js
+++ b/spec/frontend/google_cloud/gcp_regions/list_spec.js
@@ -18,10 +18,6 @@ describe('google_cloud/gcp_regions/list', () => {
wrapper = mount(GcpRegionsList, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the empty state component', () => {
expect(findEmptyState().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/service_accounts/form_spec.js b/spec/frontend/google_cloud/service_accounts/form_spec.js
index 8be481774fa..c86c8876b15 100644
--- a/spec/frontend/google_cloud/service_accounts/form_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/form_spec.js
@@ -17,10 +17,6 @@ describe('google_cloud/service_accounts/form', () => {
wrapper = shallowMount(ServiceAccountsForm, { propsData, stubs: { GlFormCheckbox } });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/service_accounts/list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js
index c2bd2005b5d..ae5776081d7 100644
--- a/spec/frontend/google_cloud/service_accounts/list_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/list_spec.js
@@ -18,10 +18,6 @@ describe('google_cloud/service_accounts/list', () => {
wrapper = mount(ServiceAccountsList, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the empty state component', () => {
expect(findEmptyState().exists()).toBe(true);
});
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index 021a3aa41ed..9cb27670c98 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('grafana integration component', () => {
let wrapper;
@@ -103,7 +103,7 @@ describe('grafana integration component', () => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates flash banner on error', async () => {
+ it('creates alert banner on error', async () => {
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 85475c749b0..e92493315f7 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -45,9 +45,6 @@ describe('group_settings/components/shared_runners_form', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
updateGroup.mockReset();
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 4e6ddd89a55..98868de8475 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -3,7 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -34,7 +34,7 @@ import {
const $toast = {
show: jest.fn(),
};
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('AppComponent', () => {
let wrapper;
@@ -65,11 +65,6 @@ describe('AppComponent', () => {
vm = wrapper.vm;
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(async () => {
mock = new AxiosMockAdapter(axios);
mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockGroups);
@@ -117,7 +112,7 @@ describe('AppComponent', () => {
});
});
- it('should show flash error when request fails', () => {
+ it('should show alert error when request fails', () => {
mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
@@ -325,7 +320,7 @@ describe('AppComponent', () => {
});
});
- it('should show error flash message if request failed to leave group', () => {
+ it('should show error alert message if request failed to leave group', () => {
const message = 'An error occurred. Please try again.';
jest
.spyOn(vm.service, 'leaveGroup')
@@ -342,7 +337,7 @@ describe('AppComponent', () => {
});
});
- it('should show appropriate error flash message if request forbids to leave group', () => {
+ it('should show appropriate error alert message if request forbids to leave group', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: HTTP_STATUS_FORBIDDEN });
jest.spyOn(vm.store, 'removeGroup');
diff --git a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
index 75edc602fbf..dc4271b98ee 100644
--- a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
@@ -24,10 +24,6 @@ const createComponent = ({ provide = {} } = {}) => {
});
};
-afterEach(() => {
- wrapper.destroy();
-});
-
const findNewSubgroupLink = () =>
wrapper.findByRole('link', {
name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title),
diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js
index f223333360d..da31fb02f69 100644
--- a/spec/frontend/groups/components/group_folder_spec.js
+++ b/spec/frontend/groups/components/group_folder_spec.js
@@ -20,10 +20,6 @@ describe('GroupFolder component', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render more children stats link when children count of group is under limit', () => {
wrapper = createComponent();
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 4570aa33a6c..663dd341a58 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -37,10 +37,6 @@ describe('GroupItemComponent', () => {
return waitForPromises();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const withMicrodata = (group) => ({
...group,
microdata: getGroupItemMicrodata(group),
diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js
index 9965b608f27..0a18e657c94 100644
--- a/spec/frontend/groups/components/group_name_and_path_spec.js
+++ b/spec/frontend/groups/components/group_name_and_path_spec.js
@@ -7,11 +7,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import GroupNameAndPath from '~/groups/components/group_name_and_path.vue';
import { getGroupPathAvailability } from '~/rest_api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { helpPagePath } from '~/helpers/help_page_helper';
import searchGroupsWhereUserCanCreateSubgroups from '~/groups/queries/search_groups_where_user_can_create_subgroups.query.graphql';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/rest_api', () => ({
getGroupPathAvailability: jest.fn(),
}));
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index cae29a8f15a..9ee785d688a 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -37,10 +37,6 @@ describe('GroupsComponent', () => {
Vue.component('GroupItem', GroupItemComponent);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('change', () => {
it('should emit `fetchPage` event when page is changed via pagination', () => {
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index 4a385cb00ee..c4bc35dcd57 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -42,8 +42,6 @@ describe('InviteMembersBanner', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
unmockTracking();
});
@@ -59,7 +57,6 @@ describe('InviteMembersBanner', () => {
});
const trackCategory = undefined;
- const buttonClickEvent = 'invite_members_banner_button_clicked';
it('sends the displayEvent when the banner is displayed', () => {
const displayEvent = 'invite_members_banner_displayed';
@@ -80,12 +77,6 @@ describe('InviteMembersBanner', () => {
source: 'invite_members_banner',
});
});
-
- it('sends the buttonClickEvent with correct trackCategory and trackLabel', () => {
- expect(trackingSpy).toHaveBeenCalledWith(trackCategory, buttonClickEvent, {
- label: provide.trackLabel,
- });
- });
});
it('sends the dismissEvent when the banner is dismissed', () => {
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
index 3ceb038dd3c..fac6fb77709 100644
--- a/spec/frontend/groups/components/item_actions_spec.js
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -18,11 +18,6 @@ describe('ItemActions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findEditGroupBtn = () => wrapper.findByTestId(`edit-group-${mockParentGroupItem.id}-btn`);
const findLeaveGroupBtn = () => wrapper.findByTestId(`leave-group-${mockParentGroupItem.id}-btn`);
const findRemoveGroupBtn = () =>
diff --git a/spec/frontend/groups/components/new_top_level_group_alert_spec.js b/spec/frontend/groups/components/new_top_level_group_alert_spec.js
index db9a5c7b16b..060663747e4 100644
--- a/spec/frontend/groups/components/new_top_level_group_alert_spec.js
+++ b/spec/frontend/groups/components/new_top_level_group_alert_spec.js
@@ -30,10 +30,6 @@ describe('NewTopLevelGroupAlert', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the component is created', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index d1ae2c4be17..906609c97f9 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -76,7 +76,6 @@ describe('OverviewTabs', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index 0065820f78f..fd0c3907e04 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -48,10 +48,6 @@ describe('Transfer group form', () => {
const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
index 77c0966ba1e..86913bb4c09 100644
--- a/spec/frontend/groups_projects/components/transfer_locations_spec.js
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -109,10 +109,6 @@ describe('TransferLocations', () => {
const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when `GlDropdown` is opened', () => {
it('shows loading icon', async () => {
getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index d6263c663d2..8e84c672d90 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -80,10 +80,6 @@ describe('HeaderSearchApp', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form');
const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findScopeToken = () => wrapper.findComponent(GlToken);
@@ -187,10 +183,10 @@ describe('HeaderSearchApp', () => {
describe.each`
username | showDropdown | expectedDesc
- ${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
- ${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
- ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
- ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
+ ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
+ ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
+ ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
+ ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
`('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
beforeEach(() => {
@@ -212,7 +208,7 @@ describe('HeaderSearchApp', () => {
${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
- ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading}
+ ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING}
`(
'Search Results Description',
({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
@@ -354,8 +350,8 @@ describe('HeaderSearchApp', () => {
describe('events', () => {
beforeEach(() => {
- createComponent();
window.gon.current_username = MOCK_USERNAME;
+ createComponent();
});
describe('Header Search Input', () => {
@@ -463,8 +459,8 @@ describe('HeaderSearchApp', () => {
${2} | ${'test1'}
`('currentFocusedOption', ({ MOCK_INDEX, search }) => {
beforeEach(() => {
- createComponent({ search });
window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search });
findHeaderSearchInput().vm.$emit('click');
});
@@ -504,8 +500,8 @@ describe('HeaderSearchApp', () => {
const MOCK_INDEX = 1;
beforeEach(() => {
- createComponent();
window.gon.current_username = MOCK_USERNAME;
+ createComponent();
findHeaderSearchInput().vm.$emit('click');
});
diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
index 7952661e2d2..e77a9231b7a 100644
--- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
@@ -3,15 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
+import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '~/header_search/constants';
import {
- GROUPS_CATEGORY,
- LARGE_AVATAR_PX,
PROJECTS_CATEGORY,
- SMALL_AVATAR_PX,
+ GROUPS_CATEGORY,
ISSUES_CATEGORY,
MERGE_REQUEST_CATEGORY,
RECENT_EPICS_CATEGORY,
-} from '~/header_search/constants';
+} from '~/vue_shared/global_search/constants';
import {
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
@@ -46,10 +45,6 @@ describe('HeaderSearchAutocompleteItems', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
const findFirstDropdownItem = () => findDropdownItems().at(0);
diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js
index abcacc487df..3768862d83e 100644
--- a/spec/frontend/header_search/components/header_search_default_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_default_items_spec.js
@@ -29,10 +29,6 @@ describe('HeaderSearchDefaultItems', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
index 2db9f71d702..51d67198f04 100644
--- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
@@ -5,7 +5,8 @@ import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
import { truncate } from '~/lib/utils/text_utility';
-import { MSG_IN_ALL_GITLAB, SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants';
+import { SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants';
+import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants';
import {
MOCK_SEARCH,
MOCK_SCOPED_SEARCH_OPTIONS,
@@ -38,10 +39,6 @@ describe('HeaderSearchScopedItems', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
index 3a8624ad9dd..2218c81efc3 100644
--- a/spec/frontend/header_search/mock_data.js
+++ b/spec/frontend/header_search/mock_data.js
@@ -1,16 +1,14 @@
+import { ICON_PROJECT, ICON_GROUP, ICON_SUBGROUP } from '~/header_search/constants';
import {
+ PROJECTS_CATEGORY,
+ GROUPS_CATEGORY,
MSG_ISSUES_ASSIGNED_TO_ME,
MSG_ISSUES_IVE_CREATED,
MSG_MR_ASSIGNED_TO_ME,
MSG_MR_IM_REVIEWER,
MSG_MR_IVE_CREATED,
MSG_IN_ALL_GITLAB,
- PROJECTS_CATEGORY,
- ICON_PROJECT,
- GROUPS_CATEGORY,
- ICON_GROUP,
- ICON_SUBGROUP,
-} from '~/header_search/constants';
+} from '~/vue_shared/global_search/constants';
export const MOCK_USERNAME = 'anyone';
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
index bd93b0edadf..95a619ebeca 100644
--- a/spec/frontend/header_search/store/actions_spec.js
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -16,7 +16,7 @@ import {
MOCK_ISSUE_PATH,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Header Search Store Actions', () => {
let state;
diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js
index a1d9481b5cc..7a7a00178f1 100644
--- a/spec/frontend/header_search/store/getters_spec.js
+++ b/spec/frontend/header_search/store/getters_spec.js
@@ -241,6 +241,13 @@ describe('Header Search Store Getters', () => {
MOCK_DEFAULT_SEARCH_OPTIONS,
);
});
+
+ it('returns the correct array if issues path is false', () => {
+ mockGetters.scopedIssuesPath = undefined;
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length),
+ );
+ });
});
describe('scopedSearchOptions', () => {
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 05161437c22..28c742386cc 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -21,17 +21,10 @@ describe('waitForCSSLoaded', () => {
});
describe('when gon features is not provided', () => {
- let originalGon;
-
beforeEach(() => {
- originalGon = window.gon;
window.gon = null;
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('should invoke the action right away', async () => {
const events = waitForCSSLoaded(mockedCallback);
await events;
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index a97e883a8bf..ff04f9a84f1 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -22,10 +22,6 @@ describe('IDE ActivityBar component', () => {
wrapper = shallowMount(ActivityBar, { store });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('updateActivityBarView', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index 3dbd1210916..4cae146cbd2 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -34,10 +34,6 @@ describe('IDE branch item', () => {
router = createRouter(store);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('if not active', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index bbde45d700f..eeab26f7559 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -35,11 +35,6 @@ describe('IDE branches search list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('calls fetch on mounted', () => {
createComponent();
expect(fetchBranchesMock).toHaveBeenCalled();
diff --git a/spec/frontend/ide/components/cannot_push_code_alert_spec.js b/spec/frontend/ide/components/cannot_push_code_alert_spec.js
index ff659ecdf3f..d4db2246008 100644
--- a/spec/frontend/ide/components/cannot_push_code_alert_spec.js
+++ b/spec/frontend/ide/components/cannot_push_code_alert_spec.js
@@ -10,10 +10,6 @@ const TEST_BUTTON_TEXT = 'Fork text';
describe('ide/components/cannot_push_code_alert', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = (props = {}) => {
wrapper = shallowMount(CannotPushCodeAlert, {
propsData: {
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index dc103fec5d0..019469cbf87 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -46,10 +46,6 @@ describe('IDE commit sidebar actions', () => {
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findText = () => wrapper.text();
const findRadios = () => wrapper.findAll('input[type="radio"]');
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index f6d5833edee..ce43e648b43 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -1,7 +1,9 @@
-import { mount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue';
+import { stubComponent } from 'helpers/stub_component';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
@@ -12,9 +14,10 @@ const TEST_FILE_PATH = 'test/file/path';
describe('IDE commit editor header', () => {
let wrapper;
let store;
+ const showMock = jest.fn();
const createComponent = (fileProps = {}) => {
- wrapper = mount(EditorHeader, {
+ wrapper = shallowMount(EditorHeader, {
store,
propsData: {
activeFile: {
@@ -23,22 +26,17 @@ describe('IDE commit editor header', () => {
...fileProps,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: { show: showMock },
+ }),
+ },
});
};
const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' });
const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' });
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockImplementation();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
fileProps | shouldExist
${{ staged: false, changed: false }} | ${false}
@@ -52,20 +50,19 @@ describe('IDE commit editor header', () => {
});
describe('discard button', () => {
- beforeEach(() => {
+ it('opens a dialog confirming discard', () => {
createComponent();
+ findDiscardButton().vm.$emit('click');
- const modal = findDiscardModal();
- jest.spyOn(modal.vm, 'show');
-
- findDiscardButton().trigger('click');
- });
-
- it('opens a dialog confirming discard', () => {
- expect(findDiscardModal().vm.show).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
it('calls discardFileChanges if dialog result is confirmed', () => {
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ createComponent();
+
expect(store.dispatch).not.toHaveBeenCalled();
findDiscardModal().vm.$emit('primary');
diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
index 7c48c0e6f95..4a6aafe42ae 100644
--- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
@@ -11,10 +11,6 @@ describe('IDE commit panel EmptyState component', () => {
wrapper = shallowMount(EmptyState, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders no changes text when last commit message is empty', () => {
expect(wrapper.find('h4').text()).toBe('No changes');
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index a8ee81afa0b..0c0998c037a 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -26,7 +26,7 @@ describe('IDE commit form', () => {
wrapper = shallowMount(CommitForm, {
store,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
GlModal: stubComponent(GlModal),
@@ -73,10 +73,6 @@ describe('IDE commit form', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
// Notes:
// - When there are no changes, there is no commit button so there's nothing to test :)
describe.each`
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index c9571d39acb..c2a33c0d71e 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -36,10 +36,6 @@ describe('Multi-file editor commit sidebar list item', () => {
findPathEl = wrapper.find('.multi-file-commit-list-path');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findPathText = () => trimText(findPathEl.text());
it('renders file path', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index 4406d14d990..6b9ba939a87 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -19,10 +19,6 @@ describe('Multi-file editor commit sidebar list', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with a list of files', () => {
beforeEach(async () => {
const f = file('file name');
diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
index c2ef29c1059..3403a7b8ad9 100644
--- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
@@ -15,10 +15,6 @@ describe('IDE commit message field', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findMessage = () => wrapper.find('textarea');
const findHighlights = () => wrapper.findAll('.highlights span');
const findMarks = () => wrapper.findAll('mark');
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 2a455c9d7c1..adc9a0f1421 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -33,15 +33,11 @@ describe('NewMergeRequestOption component', () => {
},
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the `shouldHideNewMrOption` getter returns false', () => {
beforeEach(() => {
createComponent();
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 a3fa03a4aa5..cdf14056523 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -19,15 +19,11 @@ describe('IDE commit sidebar radio group', () => {
propsData: config.props,
slots: config.slots,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without input', () => {
const props = {
value: '1',
diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
index 63d51953915..d1a81dd1639 100644
--- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
@@ -12,10 +12,6 @@ describe('IDE commit panel successful commit state', () => {
wrapper = shallowMount(SuccessMessage, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders last commit message when it exists', () => {
expect(wrapper.text()).toContain('testing commit message');
});
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
index 204d39de741..5f6579654bc 100644
--- a/spec/frontend/ide/components/error_message_spec.js
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -32,11 +32,6 @@ describe('IDE error message component', () => {
setErrorMessageMock.mockReset();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findDismissButton = () => wrapper.find('button[aria-label=Dismiss]');
const findActionButton = () => wrapper.find('button.gl-alert-action');
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index 281c549a1b4..f5a6e7222f9 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -37,8 +37,6 @@ describe('IDE extra file row component', () => {
};
afterEach(() => {
- wrapper.destroy();
-
stagedFilesCount = 0;
unstagedFilesCount = 0;
changesCount = 0;
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
index 60f37260393..b8c850fdd13 100644
--- a/spec/frontend/ide/components/file_templates/bar_spec.js
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -21,10 +21,6 @@ describe('IDE file templates bar component', () => {
wrapper = mount(Bar, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template type dropdown', () => {
it('renders dropdown component', () => {
expect(wrapper.find('.dropdown').text()).toContain('Choose a type');
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
index ee90d87357c..72fdd05eb2c 100644
--- a/spec/frontend/ide/components/file_templates/dropdown_spec.js
+++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js
@@ -49,11 +49,6 @@ describe('IDE file templates dropdown component', () => {
({ element } = wrapper);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('calls clickItem on click', async () => {
const itemData = { name: 'test.yml ' };
createComponent({ props: { data: [itemData] } });
diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js
index aa66224fa19..331877ff112 100644
--- a/spec/frontend/ide/components/ide_file_row_spec.js
+++ b/spec/frontend/ide/components/ide_file_row_spec.js
@@ -34,11 +34,6 @@ describe('Ide File Row component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findFileRowExtra = () => wrapper.findComponent(FileRowExtra);
const findFileRow = () => wrapper.findComponent(FileRow);
const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen');
diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js
index d0636352a3f..7613f407e45 100644
--- a/spec/frontend/ide/components/ide_project_header_spec.js
+++ b/spec/frontend/ide/components/ide_project_header_spec.js
@@ -20,10 +20,6 @@ describe('IDE project header', () => {
wrapper = shallowMount(IDEProjectHeader, { propsData: { project: mockProject } });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index 0759f957374..e6fd018969f 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -30,10 +30,6 @@ describe('IDE review mode', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders list of files', () => {
expect(wrapper.text()).toContain('fileName');
});
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 4784d6c516f..c258c5312d8 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -29,11 +29,6 @@ describe('IdeSidebar', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders a sidebar', () => {
wrapper = createComponent();
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
index 80e8aba4072..4ee24f63f76 100644
--- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -25,10 +25,6 @@ describe('ide/components/ide_sidebar_nav', () => {
let wrapper;
const createComponent = (props = {}) => {
- if (wrapper) {
- throw new Error('wrapper already exists');
- }
-
wrapper = shallowMount(IdeSidebarNav, {
propsData: {
tabs: TEST_TABS,
@@ -37,16 +33,11 @@ describe('ide/components/ide_sidebar_nav', () => {
...props,
},
directives: {
- tooltip: createMockDirective(),
+ tooltip: createMockDirective('tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findButtons = () => wrapper.findAll('li button');
const findButtonsData = () =>
findButtons().wrappers.map((button) => {
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index a575f428a69..1c8d570cdce 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -52,8 +52,6 @@ describe('WebIDE', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
window.onbeforeunload = null;
});
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index e6e0ebaf1e8..0ee16f98e7e 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -34,10 +34,6 @@ describe('IdeStatusBar component', () => {
wrapper = mount(IdeStatusBar, { store });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
it('triggers a setInterval', () => {
mountComponent();
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index 0b54e8b6afb..344a1fbc4f6 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -53,10 +53,7 @@ describe('ide/components/ide_status_list', () => {
});
afterEach(() => {
- wrapper.destroy();
-
store = null;
- wrapper = null;
});
describe('with regular file', () => {
diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js
index 0b9111c0e2a..3501ecce061 100644
--- a/spec/frontend/ide/components/ide_status_mr_spec.js
+++ b/spec/frontend/ide/components/ide_status_mr_spec.js
@@ -17,10 +17,6 @@ describe('ide/components/ide_status_mr', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when mounted', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index 0f61aa80e53..427daa57324 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -25,10 +25,6 @@ describe('IdeTreeList component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('normal branch', () => {
const tree = [file('fileName')];
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index f00017a2736..9f452910496 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -29,10 +29,6 @@ describe('IdeTree', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders list of files', () => {
expect(wrapper.text()).toContain('fileName');
});
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index 629c4424314..2bb0f3fccf4 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -14,10 +14,6 @@ describe('IDE job description', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders job details', () => {
expect(wrapper.text()).toContain('#1');
expect(wrapper.text()).toContain('test');
diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
index 5eb66f75978..eec1bd6b123 100644
--- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
@@ -15,10 +15,6 @@ describe('IDE job log scroll button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
direction | icon | title
${'up'} | ${'scroll_up'} | ${'Scroll to top'}
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index bf2be3aa595..60e03a7b882 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -34,10 +34,6 @@ describe('IDE jobs detail view', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mounted', () => {
const findJobOutput = () => wrapper.find('.bash');
const findBuildLoaderAnimation = () => wrapper.find('.build-loader-animation');
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index 32e27333e42..ab442a27817 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -12,10 +12,6 @@ describe('IDE jobs item', () => {
wrapper = mount(JobItem, { propsData: { job } });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders job details', () => {
expect(wrapper.text()).toContain(job.name);
expect(wrapper.text()).toContain(`#${job.id}`);
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
index 52fbff2f497..23ef92f9682 100644
--- a/spec/frontend/ide/components/jobs/stage_spec.js
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -31,11 +31,6 @@ describe('IDE pipeline stage', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('emits fetch event when mounted', () => {
createComponent();
expect(wrapper.emitted().fetch).toBeDefined();
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
index d6cf8127b53..2fbb6919b8b 100644
--- a/spec/frontend/ide/components/merge_requests/item_spec.js
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -39,11 +39,6 @@ describe('IDE merge request item', () => {
router = createRouter(store);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index ea6e2741a85..3b0e8c632fb 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -48,11 +48,6 @@ describe('IDE merge requests list', () => {
fetchMergeRequestsMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('calls fetch on mounted', () => {
createComponent();
expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js
index 8eebcdd9e08..3aae2c83e80 100644
--- a/spec/frontend/ide/components/nav_dropdown_button_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js
@@ -9,10 +9,6 @@ describe('NavDropdownButton component', () => {
const TEST_MR_ID = '12345';
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = ({ props = {}, state = {} } = {}) => {
const store = createStore();
store.replaceState(state);
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
index 33e638843f5..794aaba6d01 100644
--- a/spec/frontend/ide/components/nav_dropdown_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -30,10 +30,6 @@ describe('IDE NavDropdown', () => {
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = () => {
wrapper = mount(NavDropdown, {
store,
diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js
index a9cfdfd20c1..bfd5cdf7263 100644
--- a/spec/frontend/ide/components/new_dropdown/button_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/button_spec.js
@@ -14,10 +14,6 @@ describe('IDE new entry dropdown button component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders button with label', () => {
createComponent();
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 747c099db33..01dcb174c41 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -30,10 +30,6 @@ describe('new dropdown component', () => {
jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders new file, upload and new directory links', () => {
expect(findAllButtons().at(0).text()).toBe('New file');
expect(findAllButtons().at(1).text()).toBe('Upload file');
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index c6f9fd0c4ea..36c3d323e63 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -1,13 +1,13 @@
import { GlButton, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Modal from '~/ide/components/new_dropdown/modal.vue';
import { createStore } from '~/ide/stores';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createEntriesFromPaths } from '../../helpers';
-jest.mock('~/flash');
+jest.mock('~/alert');
const NEW_NAME = 'babar';
@@ -79,7 +79,6 @@ describe('new file modal component', () => {
afterEach(() => {
store = null;
- wrapper.destroy();
document.body.innerHTML = '';
});
@@ -94,11 +93,11 @@ describe('new file modal component', () => {
it('renders modal', () => {
expect(findGlModal().props()).toMatchObject({
actionCancel: {
- attributes: [{ variant: 'default' }],
+ attributes: { variant: 'default' },
text: 'Cancel',
},
actionPrimary: {
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
text: 'Create file',
},
actionSecondary: null,
@@ -170,7 +169,7 @@ describe('new file modal component', () => {
expect(findGlModal().props()).toMatchObject({
title: modalTitle,
actionPrimary: {
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
text: btnTitle,
},
});
@@ -298,7 +297,7 @@ describe('new file modal component', () => {
expect(findGlModal().props()).toMatchObject({
title,
actionPrimary: {
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
text: title,
},
});
@@ -340,7 +339,7 @@ describe('new file modal component', () => {
});
});
- it('does not trigger flash', () => {
+ it('does not trigger alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -359,7 +358,7 @@ describe('new file modal component', () => {
});
});
- it('does not trigger flash', () => {
+ it('does not trigger alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -379,7 +378,7 @@ describe('new file modal component', () => {
triggerSubmitModal();
});
- it('creates flash', () => {
+ it('creates alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'The name "src" is already taken in this directory.',
fadeTransition: false,
@@ -404,7 +403,7 @@ describe('new file modal component', () => {
triggerSubmitModal();
});
- it('does not create flash', () => {
+ it('does not create alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index fc643589d51..40780c7f0bd 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -12,10 +12,6 @@ describe('new dropdown upload', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('openFile', () => {
it('calls for each file', () => {
const files = ['test', 'test2', 'test3'];
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index e92f843ae6e..42eb5b3fc7a 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -35,11 +35,6 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with a tab', () => {
let fakeView;
let extensionTabs;
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 1d81c3ea89d..832983edf21 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -28,11 +28,6 @@ describe('ide/components/panes/right.vue', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js
index 31081e8f9d5..71de9aecb52 100644
--- a/spec/frontend/ide/components/pipelines/empty_state_spec.js
+++ b/spec/frontend/ide/components/pipelines/empty_state_spec.js
@@ -22,10 +22,6 @@ describe('~/ide/components/pipelines/empty_state.vue', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index d82b97561f0..e913fa84d56 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -65,11 +65,6 @@ describe('IDE pipelines list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('fetches latest pipeline', () => {
createComponent();
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index d3312358402..92bb645b1c0 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -63,11 +63,6 @@ describe('RepoCommitSection', () => {
jest.spyOn(router, 'push').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('empty state', () => {
beforeEach(() => {
store.state.noChangesStateSvgPath = TEST_NO_CHANGES_SVG;
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index c9f033bffbb..9253bfc7e71 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -162,8 +162,6 @@ describe('RepoEditor', () => {
// create a new model each time, otherwise tests conflict with each other
// because of same model being used in multiple tests
monacoEditor.getModels().forEach((model) => model.dispose());
- wrapper.destroy();
- wrapper = null;
});
describe('default', () => {
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index b26edc5a85b..b329baea783 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -37,11 +37,6 @@ describe('RepoTab', () => {
jest.spyOn(router, 'push').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders a close link and a name link', () => {
createComponent({
tab: file(),
diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index 1cfc1f12745..06ad162d398 100644
--- a/spec/frontend/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
@@ -25,10 +25,6 @@ describe('RepoTabs', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a list of tabs', async () => {
store.state.openFiles[0].active = true;
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
index fe2a128c9c8..240e675a38e 100644
--- a/spec/frontend/ide/components/resizable_panel_spec.js
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -19,11 +19,6 @@ describe('~/ide/components/resizable_panel', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createComponent = (props = {}) => {
wrapper = shallowMount(ResizablePanel, {
propsData: {
diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js
index 94da06f4cb2..186b1997497 100644
--- a/spec/frontend/ide/components/shared/commit_message_field_spec.js
+++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js
@@ -23,10 +23,6 @@ describe('CommitMessageField', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTextArea = () => wrapper.find('textarea');
const findHighlights = () => wrapper.findByTestId('highlights');
const findHighlightsText = () => wrapper.findByTestId('highlights-text');
diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js
index b70c9659e46..4bd5a6527e2 100644
--- a/spec/frontend/ide/components/shared/tokened_input_spec.js
+++ b/spec/frontend/ide/components/shared/tokened_input_spec.js
@@ -28,10 +28,6 @@ describe('IDE shared/TokenedInput', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders tokens', () => {
createComponent();
const renderedTokens = getTokenElements(wrapper).wrappers.map((w) => w.text());
diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js
index 15fb0fe9013..3a691c151d5 100644
--- a/spec/frontend/ide/components/terminal/empty_state_spec.js
+++ b/spec/frontend/ide/components/terminal/empty_state_spec.js
@@ -16,10 +16,6 @@ describe('IDE TerminalEmptyState', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not show illustration, if no path specified', () => {
factory();
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index 0d22f7f73fe..0500c116d23 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -59,10 +59,6 @@ describe('IDE Terminal', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading text', () => {
[STARTING, PENDING].forEach((status) => {
it(`shows when starting (${status})`, () => {
diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js
index 57c8da9f5b7..b8ffaa89047 100644
--- a/spec/frontend/ide/components/terminal/view_spec.js
+++ b/spec/frontend/ide/components/terminal/view_spec.js
@@ -59,10 +59,6 @@ describe('IDE TerminalView', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders empty state', async () => {
await factory();
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
index 5b1502cc190..e420e28c7b6 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
@@ -22,10 +22,6 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => {
beforeEach(createComponent);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with terminal sync module in store', () => {
beforeEach(() => {
store.registerModule('terminalSync', {
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
index 147235abc8e..4541c3b5ec8 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
@@ -48,10 +48,6 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when doing nothing', () => {
it('shows nothing', () => {
createComponent();
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 623dee387e5..cd099e60070 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -252,12 +252,10 @@ describe('IDE services', () => {
describe('pingUsage', () => {
let mock;
- let relativeUrlRoot;
const TEST_RELATIVE_URL_ROOT = 'blah-blah';
beforeEach(() => {
jest.spyOn(axios, 'post');
- relativeUrlRoot = gon.relative_url_root;
gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
mock = new MockAdapter(axios);
@@ -265,7 +263,6 @@ describe('IDE services', () => {
afterEach(() => {
mock.restore();
- gon.relative_url_root = relativeUrlRoot;
});
it('posts to usage endpoint', () => {
diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js
index 5f752197e13..5b6b60a250c 100644
--- a/spec/frontend/ide/services/terminals_spec.js
+++ b/spec/frontend/ide/services/terminals_spec.js
@@ -9,7 +9,6 @@ const TEST_BRANCH = 'ref';
describe('~/ide/services/terminals', () => {
let axiosSpy;
let mock;
- const prevRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]);
@@ -19,7 +18,6 @@ describe('~/ide/services/terminals', () => {
});
afterEach(() => {
- gon.relative_url_root = prevRelativeUrlRoot;
mock.restore();
});
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 90ca8526698..7f4e1cf761d 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -16,7 +16,6 @@ const RELATIVE_URL_ROOT = '/gitlab';
describe('IDE store file actions', () => {
let mock;
- let originalGon;
let store;
let router;
@@ -24,9 +23,7 @@ describe('IDE store file actions', () => {
stubPerformanceWebAPI();
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = {
- ...window.gon,
relative_url_root: RELATIVE_URL_ROOT,
};
@@ -44,7 +41,6 @@ describe('IDE store file actions', () => {
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('closeFile', () => {
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index fbae84631ee..a41ffdb0a31 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -3,7 +3,7 @@ import { range } from 'lodash';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
@@ -30,7 +30,7 @@ const createMergeRequestChangesCount = (n) =>
const testGetUrlForPath = (path) => `${TEST_HOST}/test/${path}`;
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('IDE store merge request actions', () => {
let store;
@@ -135,7 +135,7 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError();
});
- it('flashes message, if error', () => {
+ it('shows an alert, if error', () => {
return store
.dispatch('getMergeRequestsForBranch', {
projectId: TEST_PROJECT,
@@ -519,7 +519,7 @@ describe('IDE store merge request actions', () => {
);
});
- it('flashes message, if error', () => {
+ it('shows an alert, if error', () => {
store.dispatch.mockRejectedValue();
return openMergeRequest(store, mr).catch(() => {
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index 5a5ead4c544..b13228c20f5 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import {
@@ -19,7 +19,7 @@ import {
import { logError } from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/logger');
const TEST_PROJECT_ID = 'abc/def';
@@ -104,7 +104,7 @@ describe('IDE store project actions', () => {
desc | projectPath | responseSuccess | expectedMutations
${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]}
${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]}
- ${'flashes an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]}
+ ${'alerts an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]}
`('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => {
store.state.currentProjectId = projectPath;
if (responseSuccess) {
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index 1c90c0f943a..63b63af667c 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -4,7 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub';
import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
init,
stageAllChanges,
@@ -31,7 +31,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
}));
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Multi-file store actions', () => {
let store;
@@ -210,7 +210,7 @@ describe('Multi-file store actions', () => {
expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
});
- it('creates flash message if file already exists', async () => {
+ it('creates alert message if file already exists', async () => {
const f = file('test', '1', 'blob');
store.state.trees['abcproject/mybranch'].tree = [f];
store.state.entries[f.path] = f;
@@ -927,7 +927,7 @@ describe('Multi-file store actions', () => {
expect(document.querySelector('.flash-alert')).toBeNull();
});
- it('does not pass the error further and flashes an alert if error is not 404', async () => {
+ it('does not pass the error further and creates an alert if error is not 404', async () => {
mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_IM_A_TEAPOT);
await expect(getBranchData(...callParams)).rejects.toEqual(
diff --git a/spec/frontend/ide/stores/extend_spec.js b/spec/frontend/ide/stores/extend_spec.js
index ffb00f9ef5b..88909999c82 100644
--- a/spec/frontend/ide/stores/extend_spec.js
+++ b/spec/frontend/ide/stores/extend_spec.js
@@ -6,12 +6,10 @@ jest.mock('~/ide/stores/plugins/terminal', () => jest.fn());
jest.mock('~/ide/stores/plugins/terminal_sync', () => jest.fn());
describe('ide/stores/extend', () => {
- let prevGon;
let store;
let el;
beforeEach(() => {
- prevGon = global.gon;
store = {};
el = {};
@@ -23,13 +21,12 @@ describe('ide/stores/extend', () => {
});
afterEach(() => {
- global.gon = prevGon;
terminalPlugin.mockClear();
terminalSyncPlugin.mockClear();
});
const withGonFeatures = (features) => {
- global.gon = { ...global.gon, features };
+ global.gon.features = features;
};
describe('terminalPlugin', () => {
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index d4166a3bd6d..0fe6a16c676 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -24,11 +24,8 @@ const TEST_FORK_PATH = '/test/fork/path';
describe('IDE store getters', () => {
let localState;
let localStore;
- let origGon;
beforeEach(() => {
- origGon = window.gon;
-
// Feature flag is defaulted to on in prod
window.gon = { features: { rejectUnsignedCommitsByGitlab: true } };
@@ -36,10 +33,6 @@ describe('IDE store getters', () => {
localState = localStore.state;
});
- afterEach(() => {
- window.gon = origGon;
- });
-
describe('activeFile', () => {
it('returns the current active file', () => {
localState.openFiles.push(file());
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 4068a9d0919..872aa9b6e6b 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { file } from 'jest/ide/helpers';
import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import eventHub from '~/ide/eventhub';
@@ -39,12 +40,14 @@ describe('IDE commit module actions', () => {
let mock;
let store;
let router;
+ let trackingSpy;
beforeEach(() => {
store = createStore();
router = createRouter(store);
gon.api_version = 'v1';
mock = new MockAdapter(axios);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
jest.spyOn(router, 'push').mockImplementation();
mock
@@ -53,7 +56,7 @@ describe('IDE commit module actions', () => {
});
afterEach(() => {
- delete gon.api_version;
+ unmockTracking();
mock.restore();
});
@@ -81,19 +84,12 @@ describe('IDE commit module actions', () => {
});
describe('updateBranchName', () => {
- let originalGon;
-
beforeEach(() => {
- originalGon = window.gon;
- window.gon = { current_username: 'johndoe' };
+ window.gon.current_username = 'johndoe';
store.state.currentBranchId = 'main';
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('updates store with new branch name', async () => {
await store.dispatch('commit/updateBranchName', 'branch-name');
@@ -430,6 +426,28 @@ describe('IDE commit module actions', () => {
});
});
});
+
+ describe('learnGitlabSource', () => {
+ describe('learnGitlabSource is true', () => {
+ it('tracks commit', async () => {
+ store.state.learnGitlabSource = true;
+
+ await store.dispatch('commit/commitChanges');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'commit', {
+ label: 'web_ide_learn_gitlab_source',
+ });
+ });
+ });
+
+ describe('learnGitlabSource is false', () => {
+ it('does not track commit', async () => {
+ await store.dispatch('commit/commitChanges');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
describe('success response with failed message', () => {
@@ -447,6 +465,26 @@ describe('IDE commit module actions', () => {
expect(alert.textContent.trim()).toBe('failed message');
});
+
+ describe('learnGitlabSource', () => {
+ describe('learnGitlabSource is true', () => {
+ it('does not track commit', async () => {
+ store.state.learnGitlabSource = true;
+
+ await store.dispatch('commit/commitChanges');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('learnGitlabSource is false', () => {
+ it('does not track commit', async () => {
+ await store.dispatch('commit/commitChanges');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
describe('failed response', () => {
@@ -466,6 +504,26 @@ describe('IDE commit module actions', () => {
['commit/SET_ERROR', createUnexpectedCommitError(), undefined],
]);
});
+
+ describe('learnGitlabSource', () => {
+ describe('learnGitlabSource is true', () => {
+ it('does not track commit', async () => {
+ store.state.learnGitlabSource = true;
+
+ await store.dispatch('commit/commitChanges').catch(() => {});
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('learnGitlabSource is false', () => {
+ it('does not track commit', async () => {
+ await store.dispatch('commit/commitChanges').catch(() => {});
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
describe('first commit of a branch', () => {
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index 0287e5269ee..3f7ded5e718 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -13,7 +13,7 @@ import {
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_PROJECT_PATH = 'lorem/root';
const TEST_BRANCH_ID = 'main';
@@ -91,7 +91,7 @@ describe('IDE store terminal session controls actions', () => {
});
describe('receiveStartSessionError', () => {
- it('flashes message', () => {
+ it('shows an alert', () => {
actions.receiveStartSessionError({ dispatch });
expect(createAlert).toHaveBeenCalledWith({
@@ -165,7 +165,7 @@ describe('IDE store terminal session controls actions', () => {
});
describe('receiveStopSessionError', () => {
- it('flashes message', () => {
+ it('shows an alert', () => {
actions.receiveStopSessionError({ dispatch });
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index 9616733f052..30ae7d203a9 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -8,7 +8,7 @@ import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_SESSION = {
id: 7,
@@ -113,7 +113,7 @@ describe('IDE store terminal session controls actions', () => {
});
describe('receiveSessionStatusError', () => {
- it('flashes message', () => {
+ it('shows an alert', () => {
actions.receiveSessionStatusError({ dispatch });
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index 31e097cfa7b..b44bc33de6f 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -64,10 +64,6 @@ describe('Import entities group dropdown component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('passes namespaces from graphql query to default slot', async () => {
createComponent();
jest.advanceTimersByTime(DEBOUNCE_DELAY);
diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js
index 56c4ed827d7..3488d9f60c8 100644
--- a/spec/frontend/import_entities/components/import_status_spec.js
+++ b/spec/frontend/import_entities/components/import_status_spec.js
@@ -12,10 +12,6 @@ describe('Import entities status component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('success status', () => {
const getStatusText = () => wrapper.findComponent(GlBadge).text();
const getStatusIcon = () => wrapper.findComponent(GlBadge).props('icon');
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index 163a60bae36..1a52485f779 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdown, GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
@@ -8,7 +8,6 @@ describe('import actions cell', () => {
const createComponent = (props) => {
wrapper = shallowMount(ImportActionsCell, {
propsData: {
- isProjectsImportEnabled: false,
isFinished: false,
isAvailableForImport: false,
isInvalid: false,
@@ -17,19 +16,15 @@ describe('import actions cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when group is available for import', () => {
beforeEach(() => {
createComponent({ isAvailableForImport: true });
});
- it('renders import button', () => {
- const button = wrapper.findComponent(GlButton);
- expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Import');
+ it('renders import dropdown', () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('text')).toBe('Import with projects');
});
it('does not render icon with a hint', () => {
@@ -42,10 +37,10 @@ describe('import actions cell', () => {
createComponent({ isAvailableForImport: false, isFinished: true });
});
- it('renders re-import button', () => {
- const button = wrapper.findComponent(GlButton);
- expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Re-import');
+ it('renders re-import dropdown', () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('text')).toBe('Re-import with projects');
});
it('renders icon with a hint', () => {
@@ -57,25 +52,25 @@ describe('import actions cell', () => {
});
});
- it('does not render import button when group is not available for import', () => {
+ it('does not render import dropdown when group is not available for import', () => {
createComponent({ isAvailableForImport: false });
- const button = wrapper.findComponent(GlButton);
- expect(button.exists()).toBe(false);
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.exists()).toBe(false);
});
- it('renders import button as disabled when group is invalid', () => {
+ it('renders import dropdown as disabled when group is invalid', () => {
createComponent({ isInvalid: true, isAvailableForImport: true });
- const button = wrapper.findComponent(GlButton);
- expect(button.props().disabled).toBe(true);
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.props().disabled).toBe(true);
});
it('emits import-group event when import button is clicked', () => {
createComponent({ isAvailableForImport: true });
- const button = wrapper.findComponent(GlButton);
- button.vm.$emit('click');
+ const dropdown = wrapper.findComponent(GlDropdown);
+ dropdown.vm.$emit('click');
expect(wrapper.emitted('import-group')).toHaveLength(1);
});
@@ -85,10 +80,10 @@ describe('import actions cell', () => {
${false} | ${'Import'}
${true} | ${'Re-import'}
`(
- 'when import projects is enabled, group is available for import and finish status is $status',
+ 'group is available for import and finish status is $isFinished',
({ isFinished, expectedAction }) => {
beforeEach(() => {
- createComponent({ isProjectsImportEnabled: true, isAvailableForImport: true, isFinished });
+ createComponent({ isAvailableForImport: true, isFinished });
});
it('render import dropdown', () => {
diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
index f2735d86493..9ead483d02f 100644
--- a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
@@ -22,10 +22,6 @@ describe('import source cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when group status is NONE', () => {
beforeEach(() => {
group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
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 c7bda5a60ec..205218fdabd 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
@@ -6,7 +6,7 @@ import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK, HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
@@ -23,7 +23,7 @@ import {
generateFakeEntry,
} from '../graphql/fixtures';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/import_entities/import_groups/services/status_poller');
Vue.use(VueApollo);
@@ -49,12 +49,12 @@ describe('import table', () => {
},
};
- const findImportSelectedButton = () =>
- wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected');
const findImportSelectedDropdown = () =>
- wrapper.findAll('.gl-dropdown').wrappers.find((w) => w.text().includes('Import with projects'));
- const findImportButtons = () =>
- wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
+ wrapper.find('[data-testid="import-selected-groups-dropdown"]');
+ const findRowImportDropdownAtIndex = (idx) =>
+ wrapper.findAll('tbody td button').wrappers.filter((w) => w.text() === 'Import with projects')[
+ idx
+ ];
const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]');
const findTargetNamespaceDropdown = (rowWrapper) =>
rowWrapper.find('[data-testid="target-namespace-selector"]');
@@ -70,12 +70,7 @@ describe('import table', () => {
const findRowCheckbox = (idx) => wrapper.findAll('tbody td input[type=checkbox]').at(idx);
const selectRow = (idx) => findRowCheckbox(idx).setChecked(true);
- const createComponent = ({
- bulkImportSourceGroups,
- importGroups,
- defaultTargetNamespace,
- glFeatures = {},
- }) => {
+ const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => {
apolloProvider = createMockApollo(
[
[
@@ -102,10 +97,7 @@ describe('import table', () => {
defaultTargetNamespace,
},
directives: {
- GlTooltip: createMockDirective(),
- },
- provide: {
- glFeatures,
+ GlTooltip: createMockDirective('gl-tooltip'),
},
apolloProvider,
});
@@ -120,10 +112,6 @@ describe('import table', () => {
axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK, { exists: false });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('renders loading icon while performing request', async () => {
createComponent({
@@ -134,7 +122,7 @@ describe('import table', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('does not renders loading icon when request is completed', async () => {
+ it('does not render loading icon when request is completed', async () => {
createComponent({
bulkImportSourceGroups: () => [],
});
@@ -245,12 +233,13 @@ describe('import table', () => {
await waitForPromises();
- await findImportButtons()[0].trigger('click');
+ await findRowImportDropdownAtIndex(0).trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
variables: {
importRequests: [
{
+ migrateProjects: true,
newName: FAKE_GROUP.lastImportTarget.newName,
sourceGroupId: FAKE_GROUP.id,
targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
@@ -273,7 +262,7 @@ describe('import table', () => {
});
await waitForPromises();
- await findImportButtons()[0].trigger('click');
+ await findRowImportDropdownAtIndex(0).trigger('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(
@@ -298,7 +287,7 @@ describe('import table', () => {
});
await waitForPromises();
- await findImportButtons()[0].trigger('click');
+ await findRowImportDropdownAtIndex(0).trigger('click');
await waitForPromises();
expect(createAlert).not.toHaveBeenCalled();
@@ -476,7 +465,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(findImportSelectedButton().props().disabled).toBe(true);
+ expect(findImportSelectedDropdown().props().disabled).toBe(true);
});
it('import selected button is enabled when groups were selected for import', async () => {
@@ -491,7 +480,7 @@ describe('import table', () => {
await selectRow(0);
- expect(findImportSelectedButton().props().disabled).toBe(false);
+ expect(findImportSelectedDropdown().props().disabled).toBe(false);
});
it('does not allow selecting already started groups', async () => {
@@ -509,7 +498,7 @@ describe('import table', () => {
await selectRow(0);
await nextTick();
- expect(findImportSelectedButton().props().disabled).toBe(true);
+ expect(findImportSelectedDropdown().props().disabled).toBe(true);
});
it('does not allow selecting groups with validation errors', async () => {
@@ -534,10 +523,10 @@ describe('import table', () => {
await selectRow(0);
await nextTick();
- expect(findImportSelectedButton().props().disabled).toBe(true);
+ expect(findImportSelectedDropdown().props().disabled).toBe(true);
});
- it('invokes importGroups mutation when import selected button is clicked', async () => {
+ it('invokes importGroups mutation when import selected dropdown is clicked', async () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
@@ -558,7 +547,7 @@ describe('import table', () => {
await selectRow(1);
await nextTick();
- await findImportSelectedButton().trigger('click');
+ await findImportSelectedDropdown().find('button').trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
@@ -679,7 +668,7 @@ describe('import table', () => {
});
});
- describe('when import projects is enabled', () => {
+ describe('importing projects', () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
@@ -693,9 +682,6 @@ describe('import table', () => {
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
- glFeatures: {
- bulkImportProjects: true,
- },
});
jest.spyOn(apolloProvider.defaultClient, 'mutate');
return waitForPromises();
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 d5286e71c44..a524d9ebdb0 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
@@ -57,11 +57,6 @@ describe('import target cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('events', () => {
beforeEach(async () => {
group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index ce111a0c10c..83566469176 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -16,7 +16,7 @@ import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { statusEndpointFixture } from './fixtures';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({
LocalStorageCache: jest.fn().mockImplementation(function mock() {
this.get = jest.fn();
diff --git a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 4a1b85d24e3..5ee2b2e698f 100644
--- a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Visibility from 'visibilityjs';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { STATUSES } from '~/import_entities/constants';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import axios from '~/lib/utils/axios_utils';
@@ -8,7 +8,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
jest.mock('visibilityjs');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/poll');
const FAKE_POLL_PATH = '/fake/poll/path';
@@ -81,7 +81,7 @@ describe('Bulk import status poller', () => {
expect(pollInstance.makeRequest).toHaveBeenCalled();
});
- it('when error occurs shows flash with error', () => {
+ it('when error occurs shows alert with error', () => {
const [[pollConfig]] = Poll.mock.calls;
pollConfig.errorCallback();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
index 68716600592..2294d236e8b 100644
--- a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
@@ -25,10 +25,6 @@ describe('Import Advanced Settings', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders GLFormCheckbox for each optional stage', () => {
expect(wrapper.findAllComponents(GlFormCheckbox)).toHaveLength(OPTIONAL_STAGES.length);
});
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 e613b9756af..8e73f76382a 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
@@ -60,11 +60,6 @@ describe('ProviderRepoTableRow', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when rendering importable project', () => {
const repo = {
importSource: {
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index 990587d4af7..f78016eefcf 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { STATUSES, PROVIDERS } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
@@ -27,7 +27,7 @@ import {
HTTP_STATUS_TOO_MANY_REQUESTS,
} from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
const MOCK_ENDPOINT = `${TEST_HOST}/endpoint.json`;
const endpoints = {
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 1d1b285c1b6..c5c29b4bb19 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,12 +1,12 @@
import AxiosMockAdapter from 'axios-mock-adapter';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('IncidentsSettingsService', () => {
@@ -33,7 +33,7 @@ describe('IncidentsSettingsService', () => {
});
});
- it('should display a flash message on update error', () => {
+ it('should display an alert message on update error', () => {
mock.onPatch().reply(HTTP_STATUS_BAD_REQUEST);
return service.updateSettings({}).then(() => {
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index 521a861829b..77258db437d 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -26,10 +26,6 @@ describe('Alert integration settings form', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
index 1f7a5f0dbc9..8afff842a85 100644
--- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js
+++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
@@ -17,10 +17,6 @@ describe('ActiveCheckbox', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findInputInCheckbox = () => findGlFormCheckbox().find('input');
diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
index cbe3402727a..dfb6b7d9a9c 100644
--- a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
+++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
@@ -14,10 +14,6 @@ describe('ConfirmationModal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlModal = () => wrapper.findComponent(GlModal);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 7589b04b0fd..e1d9aef752f 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -21,10 +21,6 @@ describe('DynamicField', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findGlFormInput = () => wrapper.findComponent(GlFormInput);
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 383dfb36aa5..58fb456eb53 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -84,7 +84,6 @@ describe('IntegrationForm', () => {
});
afterEach(() => {
- wrapper.destroy();
mockAxios.restore();
});
@@ -507,30 +506,21 @@ describe('IntegrationForm', () => {
const dummyHelp = 'Foo Help';
it.each`
- integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
- ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ integration | helpHtml | sections | shouldShowSections | shouldShowHelp
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
+ ${'foo'} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
`(
- '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
- ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ '$sections sections, and "$helpHtml" helpHtml for "$integration" integration',
+ ({ integration, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
createComponent({
provide: {
helpHtml,
- glFeatures: { integrationSlackAppNotifications: flagIsOn },
},
customStateProps: {
sections,
@@ -553,20 +543,15 @@ describe('IntegrationForm', () => {
${false} | ${true} | ${'When having only the fields without a section'}
`('$description', ({ hasSections, hasFieldsWithoutSections }) => {
it.each`
- prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert
- ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true}
- ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${true} | ${false}
- ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${false} | ${false}
- ${'does not'} | ${'foo'} | ${true} | ${true} | ${false}
- ${'does not'} | ${'foo'} | ${false} | ${true} | ${false}
- ${'does not'} | ${'foo'} | ${true} | ${false} | ${false}
+ prefix | integration | shouldUpgradeSlack | shouldShowAlert
+ ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true}
+ ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${false}
+ ${'does not'} | ${'foo'} | ${true} | ${false}
+ ${'does not'} | ${'foo'} | ${false} | ${false}
`(
- '$prefix render the upgrade warning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections',
- ({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => {
+ '$prefix render the upgrade warning when we are in "$integration" integration with Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections',
+ ({ integration, shouldUpgradeSlack, shouldShowAlert }) => {
createComponent({
- provide: {
- glFeatures: { integrationSlackAppNotifications: flagIsOn },
- },
customStateProps: {
shouldUpgradeSlack,
type: integration,
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index fa91f8de45a..90ee69ef2dc 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -31,10 +31,6 @@ describe('JiraIssuesFields', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findEnableCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
index 6011b3e6edc..f876a497f98 100644
--- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -22,10 +22,6 @@ describe('JiraTriggerFields', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCommentSettings = () => wrapper.findByTestId('comment-settings');
const findCommentDetail = () => wrapper.findByTestId('comment-detail');
const findCommentSettingsCheckbox = () => findCommentSettings().findComponent(GlFormCheckbox);
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
index 90facaff1f9..2d1a6b3ace1 100644
--- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -26,10 +26,6 @@ describe('OverrideDropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlLink = () => wrapper.findComponent(GlLink);
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js
new file mode 100644
index 00000000000..62f0439a13f
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionAppleAppStore from '~/integrations/edit/components/sections/apple_app_store.vue';
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+describe('IntegrationSectionAppleAppStore', () => {
+ let wrapper;
+
+ const createComponent = (componentFields) => {
+ const store = createStore({
+ customState: { ...componentFields },
+ });
+ wrapper = shallowMount(IntegrationSectionAppleAppStore, {
+ store,
+ });
+ };
+
+ const componentFields = (fileName = '') => {
+ return {
+ fields: [
+ {
+ name: 'app_store_private_key_file_name',
+ value: fileName,
+ },
+ ],
+ };
+ };
+
+ const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField);
+
+ describe('computed properties', () => {
+ it('renders UploadDropzoneField with default values', () => {
+ createComponent(componentFields());
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'The Apple App Store Connect Private Key (.p8)',
+ helpText: '',
+ });
+ });
+
+ it('renders UploadDropzoneField with custom values for an attached file', () => {
+ createComponent(componentFields('fileName.txt'));
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Upload a new Apple App Store Connect Private Key (replace fileName.txt)',
+ helpText: 'Leave empty to use your current Private Key.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/sections/configuration_spec.js b/spec/frontend/integrations/edit/components/sections/configuration_spec.js
index e697212ea0b..c8a7d17c041 100644
--- a/spec/frontend/integrations/edit/components/sections/configuration_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/configuration_spec.js
@@ -19,10 +19,6 @@ describe('IntegrationSectionCoonfiguration', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/connection_spec.js b/spec/frontend/integrations/edit/components/sections/connection_spec.js
index 1eb92e80723..a24253d542d 100644
--- a/spec/frontend/integrations/edit/components/sections/connection_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/connection_spec.js
@@ -20,10 +20,6 @@ describe('IntegrationSectionConnection', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
diff --git a/spec/frontend/integrations/edit/components/sections/google_play_spec.js b/spec/frontend/integrations/edit/components/sections/google_play_spec.js
new file mode 100644
index 00000000000..c0d6d17f639
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/google_play_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionGooglePlay from '~/integrations/edit/components/sections/google_play.vue';
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+describe('IntegrationSectionGooglePlay', () => {
+ let wrapper;
+
+ const createComponent = (fileName = '') => {
+ const store = createStore({
+ customState: {
+ fields: [
+ {
+ name: 'service_account_key_file_name',
+ value: fileName,
+ },
+ ],
+ },
+ });
+
+ wrapper = shallowMount(IntegrationSectionGooglePlay, {
+ store,
+ });
+ };
+
+ const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField);
+
+ describe('computed properties', () => {
+ it('renders UploadDropzoneField with default values', () => {
+ createComponent();
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Service account key (.json)',
+ helpText: '',
+ });
+ });
+
+ it('renders UploadDropzoneField with custom values for an attached file', () => {
+ createComponent('fileName.txt');
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Upload a new service account key (replace fileName.txt)',
+ helpText: 'Leave empty to use your current service account key.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
index a7c1cc2a03f..8b39fa8f583 100644
--- a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
@@ -18,10 +18,6 @@ describe('IntegrationSectionJiraIssue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
index d4ab9864fab..b3b7f508e25 100644
--- a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
@@ -18,10 +18,6 @@ describe('IntegrationSectionJiraTrigger', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/trigger_spec.js b/spec/frontend/integrations/edit/components/sections/trigger_spec.js
index 883f5c7bf79..b9c1efbb0a2 100644
--- a/spec/frontend/integrations/edit/components/sections/trigger_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/trigger_spec.js
@@ -18,10 +18,6 @@ describe('IntegrationSectionTrigger', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAllTriggerFields = () => wrapper.findAllComponents(TriggerField);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/trigger_field_spec.js b/spec/frontend/integrations/edit/components/trigger_field_spec.js
index ed0b3324708..3b736b33a2f 100644
--- a/spec/frontend/integrations/edit/components/trigger_field_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_field_spec.js
@@ -23,10 +23,6 @@ describe('TriggerField', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findGlFormInput = () => wrapper.findComponent(GlFormInput);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 082eeea30f1..defa02aefd2 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -20,10 +20,6 @@ describe('TriggerFields', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label');
const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAllComponents(GlFormGroup);
const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
diff --git a/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js
new file mode 100644
index 00000000000..36e20db0022
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js
@@ -0,0 +1,88 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import { mockField } from '../mock_data';
+
+describe('UploadDropzoneField', () => {
+ let wrapper;
+
+ const contentsInputName = 'service[app_store_private_key]';
+ const fileNameInputName = 'service[app_store_private_key_file_name]';
+
+ const createComponent = (props) => {
+ wrapper = mount(UploadDropzoneField, {
+ propsData: {
+ ...mockField,
+ ...props,
+ name: contentsInputName,
+ label: 'Input Label',
+ fileInputName: fileNameInputName,
+ },
+ });
+ };
+
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+ const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
+ const findFileContentsHiddenInput = () => wrapper.find(`input[name="${contentsInputName}"]`);
+ const findFileNameHiddenInput = () => wrapper.find(`input[name="${fileNameInputName}"]`);
+
+ describe('template', () => {
+ it('adds the expected file inputFieldName', () => {
+ createComponent();
+
+ expect(findUploadDropzone().props('inputFieldName')).toBe('service[dropzone_file_name]');
+ });
+
+ it('adds a disabled, hidden text input for the file contents', () => {
+ createComponent();
+
+ expect(findFileContentsHiddenInput().attributes('name')).toBe(contentsInputName);
+ expect(findFileContentsHiddenInput().attributes('disabled')).toBeDefined();
+ });
+
+ it('adds a disabled, hidden text input for the file name', () => {
+ createComponent();
+
+ expect(findFileNameHiddenInput().attributes('name')).toBe(fileNameInputName);
+ expect(findFileNameHiddenInput().attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('clearError', () => {
+ it('clears uploadError when called', async () => {
+ createComponent();
+
+ expect(findGlAlert().exists()).toBe(false);
+
+ findUploadDropzone().vm.$emit('error');
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(
+ 'Error: You are trying to upload something other than an allowed file.',
+ );
+
+ findGlAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('onError', () => {
+ it('assigns uploadError to the supplied custom message', async () => {
+ const message = 'test error message';
+ createComponent({ errorMessage: message });
+
+ findUploadDropzone().vm.$emit('error');
+
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(message);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/index/components/integrations_list_spec.js b/spec/frontend/integrations/index/components/integrations_list_spec.js
index ee54a5fd359..155a3d1c6be 100644
--- a/spec/frontend/integrations/index/components/integrations_list_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_list_spec.js
@@ -13,10 +13,6 @@ describe('IntegrationsList', () => {
wrapper = shallowMountExtended(IntegrationsList, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('provides correct `integrations` prop to the IntegrationsTable instance', () => {
createComponent({ integrations: [...mockInactiveIntegrations, ...mockActiveIntegrations] });
diff --git a/spec/frontend/integrations/index/components/integrations_table_spec.js b/spec/frontend/integrations/index/components/integrations_table_spec.js
index 976c7b74890..5456a23a98d 100644
--- a/spec/frontend/integrations/index/components/integrations_table_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_table_spec.js
@@ -1,6 +1,5 @@
-import { GlTable, GlIcon, GlLink } from '@gitlab/ui';
+import { GlTable, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { INTEGRATION_TYPE_SLACK } from '~/integrations/constants';
import IntegrationsTable from '~/integrations/index/components/integrations_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -11,24 +10,15 @@ describe('IntegrationsTable', () => {
const findTable = () => wrapper.findComponent(GlTable);
- const createComponent = (propsData = {}, flagIsOn = false) => {
+ const createComponent = (propsData = {}) => {
wrapper = mount(IntegrationsTable, {
propsData: {
integrations: mockActiveIntegrations,
...propsData,
},
- provide: {
- glFeatures: {
- integrationSlackAppNotifications: flagIsOn,
- },
- },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each([true, false])('when `showUpdatedAt` is %p', (showUpdatedAt) => {
beforeEach(() => {
createComponent({ showUpdatedAt });
@@ -56,51 +46,4 @@ describe('IntegrationsTable', () => {
expect(findTable().findComponent(GlIcon).exists()).toBe(shouldRenderActiveIcon);
});
});
-
- describe('integrations filtering', () => {
- const slackActive = {
- ...mockActiveIntegrations[0],
- name: INTEGRATION_TYPE_SLACK,
- title: 'Slack',
- };
- const slackInactive = {
- ...mockInactiveIntegrations[0],
- name: INTEGRATION_TYPE_SLACK,
- title: 'Slack',
- };
-
- describe.each`
- desc | flagIsOn | integrations | expectedIntegrations
- ${'only active'} | ${false} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
- ${'only active'} | ${true} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
- ${'only inactive'} | ${true} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
- ${'only inactive'} | ${false} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
- ${'active and inactive'} | ${true} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'active and inactive'} | ${false} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack active with active'} | ${false} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
- ${'Slack active with active'} | ${true} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
- ${'Slack active with inactive'} | ${false} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
- ${'Slack active with inactive'} | ${true} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
- ${'Slack inactive with active'} | ${false} | ${[slackInactive, ...mockActiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations]}
- ${'Slack inactive with active'} | ${true} | ${[slackInactive, ...mockActiveIntegrations]} | ${mockActiveIntegrations}
- ${'Slack inactive with inactive'} | ${false} | ${[slackInactive, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockInactiveIntegrations]}
- ${'Slack inactive with inactive'} | ${true} | ${[slackInactive, ...mockInactiveIntegrations]} | ${mockInactiveIntegrations}
- ${'Slack active with active and inactive'} | ${true} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack active with active and inactive'} | ${false} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack inactive with active and inactive'} | ${true} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack inactive with active and inactive'} | ${false} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
- `('when $desc and flag "$flagIsOn"', ({ flagIsOn, integrations, expectedIntegrations }) => {
- beforeEach(() => {
- createComponent({ integrations }, flagIsOn);
- });
-
- it('renders correctly', () => {
- const links = wrapper.findAllComponents(GlLink);
- expect(links).toHaveLength(expectedIntegrations.length);
- expectedIntegrations.forEach((integration, index) => {
- expect(links.at(index).text()).toBe(integration.title);
- });
- });
- });
- });
});
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index fdb728281b5..9e863eaecfd 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -47,7 +47,6 @@ describe('IntegrationOverrides', () => {
afterEach(() => {
mockAxios.restore();
- wrapper.destroy();
});
const findGlTable = () => wrapper.findComponent(GlTable);
diff --git a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
index a728b4d391f..b35a40d69c1 100644
--- a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
@@ -21,10 +21,6 @@ describe('IntegrationTabs', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlTab = () => wrapper.findComponent(GlTab);
const findSettingsLink = () => wrapper.find('a');
diff --git a/spec/frontend/invite_members/components/confetti_spec.js b/spec/frontend/invite_members/components/confetti_spec.js
index 2f361f1dc1e..382569abfd9 100644
--- a/spec/frontend/invite_members/components/confetti_spec.js
+++ b/spec/frontend/invite_members/components/confetti_spec.js
@@ -6,16 +6,10 @@ jest.mock('canvas-confetti', () => ({
create: jest.fn(),
}));
-let wrapper;
-
const createComponent = () => {
- wrapper = shallowMount(Confetti);
+ shallowMount(Confetti);
};
-afterEach(() => {
- wrapper.destroy();
-});
-
describe('Confetti', () => {
it('initiates confetti', () => {
const basicCannon = jest.spyOn(Confetti.methods, 'basicCannon').mockImplementation(() => {});
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index e1563a7bb3a..a1ca9a69926 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -26,14 +26,9 @@ describe('GroupSelect', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]');
+ const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]');
const findAvatarByLabel = (text) =>
wrapper
.findAllComponents(GlAvatarLabeled)
@@ -66,6 +61,7 @@ describe('GroupSelect', () => {
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
exclude_internal: true,
active: true,
+ order_by: 'similarity',
});
});
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 d839cde163c..74cb59a9b52 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
@@ -54,7 +54,6 @@ beforeEach(() => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/invite_members/components/import_project_members_trigger_spec.js b/spec/frontend/invite_members/components/import_project_members_trigger_spec.js
index b6375fcfa22..0e8243491a8 100644
--- a/spec/frontend/invite_members/components/import_project_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_trigger_spec.js
@@ -17,10 +17,6 @@ const createComponent = (props = {}) => {
describe('ImportProjectMembersTrigger', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.findComponent(GlButton);
describe('displayText', () => {
diff --git a/spec/frontend/invite_members/components/invite_group_trigger_spec.js b/spec/frontend/invite_members/components/invite_group_trigger_spec.js
index 84ddb779a9e..e088dc41a2b 100644
--- a/spec/frontend/invite_members/components/invite_group_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_group_trigger_spec.js
@@ -17,11 +17,6 @@ const createComponent = (props = {}) => {
describe('InviteGroupTrigger', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findButton = () => wrapper.findComponent(GlButton);
describe('displayText', () => {
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index c2a55517405..82b4717fbf1 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -44,11 +44,6 @@ describe('InviteGroupsModal', () => {
createComponent({ isProject: false });
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
const findInviteGroupAlert = () => wrapper.findComponent(InviteGroupNotification);
@@ -58,11 +53,13 @@ describe('InviteGroupsModal', () => {
findMembersFormGroup().attributes('invalid-feedback');
const findBase = () => wrapper.findComponent(InviteModalBase);
const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
- const emitEventFromModal = (eventName) => () =>
- findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
- const hideModal = emitEventFromModal('hidden');
- const clickInviteButton = emitEventFromModal('primary');
- const clickCancelButton = emitEventFromModal('cancel');
+ const hideModal = () => findModal().vm.$emit('hidden', { preventDefault: jest.fn() });
+
+ const emitClickFromModal = (testId) => () =>
+ wrapper.findByTestId(testId).vm.$emit('click', { preventDefault: jest.fn() });
+
+ const clickInviteButton = emitClickFromModal('invite-modal-submit');
+ const clickCancelButton = emitClickFromModal('invite-modal-cancel');
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
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 9687d528321..39d5ddee723 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -73,6 +73,7 @@ describe('InviteMembersModal', () => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
+ name: propsData.name,
},
propsData: {
usersLimitDataset: {},
@@ -116,8 +117,6 @@ describe('InviteMembersModal', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mock.restore();
});
@@ -134,10 +133,15 @@ describe('InviteMembersModal', () => {
`${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${
Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]
}`;
- const emitEventFromModal = (eventName) => () =>
- findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
- const clickInviteButton = emitEventFromModal('primary');
- const clickCancelButton = emitEventFromModal('cancel');
+ const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
+ const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
+
+ const emitClickFromModal = (findButton) => () =>
+ findButton().vm.$emit('click', { preventDefault: jest.fn() });
+
+ const clickInviteButton = emitClickFromModal(findActionButton);
+ const clickCancelButton = emitClickFromModal(findCancelButton);
+
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
@@ -368,13 +372,11 @@ describe('InviteMembersModal', () => {
it('tracks actions', async () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- const mockEvent = { preventDefault: jest.fn() };
-
await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
expectTracking('render', ON_CELEBRATION_TRACK_LABEL);
- findModal().vm.$emit('cancel', mockEvent);
+ clickCancelButton();
expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL);
findModal().vm.$emit('close');
@@ -411,13 +413,11 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- const mockEvent = { preventDefault: jest.fn() };
-
await triggerOpenModal(source);
expectTracking('render', label);
- findModal().vm.$emit('cancel', mockEvent);
+ clickCancelButton();
expectTracking('click_cancel', label);
findModal().vm.$emit('close');
@@ -734,7 +734,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
});
it('clears the error when the modal is hidden', async () => {
@@ -746,7 +746,7 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
findModal().vm.$emit('hidden');
@@ -768,7 +768,7 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
});
it('displays all errors when there are multiple emails that return a restricted error message', async () => {
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index c522abe63c5..cdb6182e2ae 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,12 +1,14 @@
-import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
+import { GlButton, GlLink, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
import {
TRIGGER_ELEMENT_BUTTON,
- TRIGGER_ELEMENT_SIDE_NAV,
TRIGGER_DEFAULT_QA_SELECTOR,
+ TRIGGER_ELEMENT_WITH_EMOJI,
+ TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
} from '~/invite_members/constants';
+import { GlEmoji } from '../mock_data/member_modal';
jest.mock('~/experimentation/experiment_tracking');
@@ -19,7 +21,8 @@ let findButton;
const triggerComponent = {
button: GlButton,
anchor: GlLink,
- 'side-nav': GlLink,
+ 'text-emoji': GlLink,
+ 'dropdown-text-emoji': GlDropdownItem,
};
const createComponent = (props = {}) => {
@@ -29,6 +32,9 @@ const createComponent = (props = {}) => {
...triggerProps,
...props,
},
+ stubs: {
+ GlEmoji,
+ },
});
};
@@ -40,8 +46,8 @@ const triggerItems = [
triggerElement: 'anchor',
},
{
- triggerElement: TRIGGER_ELEMENT_SIDE_NAV,
- icon: 'plus',
+ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI,
+ icon: 'shaking_hands',
},
];
@@ -50,10 +56,6 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
findButton = () => wrapper.findComponent(triggerComponent[triggerItem.triggerElement]);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('configurable attributes', () => {
it('includes the correct displayText for the button', () => {
createComponent();
@@ -91,31 +93,26 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
});
});
});
+});
- describe('tracking', () => {
- it('does not add tracking attributes', () => {
- createComponent();
-
- expect(findButton().attributes('data-track-action')).toBeUndefined();
- expect(findButton().attributes('data-track-label')).toBeUndefined();
- });
+describe('link with emoji', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findEmoji = () => wrapper.findComponent(GlEmoji);
- it('adds tracking attributes', () => {
- createComponent({ label: '_label_', event: '_event_' });
+ createComponent({ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, icon: 'shaking_hands' });
- expect(findButton().attributes('data-track-action')).toBe('_event_');
- expect(findButton().attributes('data-track-label')).toBe('_label_');
- });
+ expect(findEmoji().exists()).toBe(true);
+ expect(findEmoji().attributes('data-name')).toBe('shaking_hands');
});
});
-describe('side-nav with icon', () => {
+describe('dropdown item with emoji', () => {
it('includes the specified icon with correct size when triggerElement is link', () => {
- const findIcon = () => wrapper.findComponent(GlIcon);
+ const findEmoji = () => wrapper.findComponent(GlEmoji);
- createComponent({ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus' });
+ createComponent({ triggerElement: TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, icon: 'shaking_hands' });
- expect(findIcon().exists()).toBe(true);
- expect(findIcon().props('name')).toBe('plus');
+ expect(findEmoji().exists()).toBe(true);
+ expect(findEmoji().attributes('data-name')).toBe('shaking_hands');
});
});
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index f34f9902514..e70c83a424e 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -54,10 +54,6 @@ describe('InviteModalBase', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const findFormSelectOptions = () => findFormSelect().findAllComponents('option');
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
@@ -66,8 +62,8 @@ describe('InviteModalBase', () => {
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const findDisabledInput = () => wrapper.findByTestId('disabled-input');
- const findCancelButton = () => wrapper.find('.js-modal-action-cancel');
- const findActionButton = () => wrapper.find('.js-modal-action-primary');
+ const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
+ const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
describe('rendering the modal', () => {
let trackingSpy;
@@ -88,20 +84,19 @@ describe('InviteModalBase', () => {
});
it('renders the Cancel button text correctly', () => {
- expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({
- text: CANCEL_BUTTON_TEXT,
- });
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
});
it('renders the Invite button correctly', () => {
- expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({
- text: INVITE_BUTTON_TEXT,
- attributes: {
- variant: 'confirm',
- disabled: false,
- loading: false,
- 'data-qa-selector': 'invite_button',
- },
+ const actionButton = findActionButton();
+
+ expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT);
+ expect(actionButton.attributes('data-qa-selector')).toBe('invite_button');
+
+ expect(actionButton.props()).toMatchObject({
+ variant: 'confirm',
+ disabled: false,
+ loading: false,
});
});
@@ -235,7 +230,7 @@ describe('InviteModalBase', () => {
},
});
- expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
+ expect(findActionButton().props('loading')).toBe(true);
});
it('with invalidFeedbackMessage, set members form group exception state', () => {
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 0455460918c..c7e9905dee3 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -30,11 +30,6 @@ const createComponent = (props) => {
describe('MembersTokenSelect', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
describe('rendering the token-selector component', () => {
diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js
index 6fbf95362fa..20db4f20408 100644
--- a/spec/frontend/invite_members/components/project_select_spec.js
+++ b/spec/frontend/invite_members/components/project_select_spec.js
@@ -23,10 +23,6 @@ describe('ProjectSelect', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findAvatarLabeled = (index) => wrapper.findAllComponents(GlAvatarLabeled).at(index);
diff --git a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
index 38b16dd0c2c..fd011658f95 100644
--- a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
+++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
@@ -6,10 +6,10 @@ import {
TOAST_MESSAGE_LOCALSTORAGE_KEY,
TOAST_MESSAGE_SUCCESSFUL,
} from '~/invite_members/constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-jest.mock('~/flash');
+jest.mock('~/alert');
useLocalStorageSpy();
describe('Display Successful Invitation Alert', () => {
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index f798f87b6b2..ccd53e64c4d 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -17,7 +17,7 @@ describe('CsvExportModal', () => {
...props,
},
provide: {
- issuableType: 'issues',
+ issuableType: 'issue',
...injectedProperties,
},
stubs: {
@@ -29,19 +29,15 @@ describe('CsvExportModal', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findIcon = () => wrapper.findComponent(GlIcon);
describe('template', () => {
describe.each`
- issuableType | modalTitle
- ${'issues'} | ${'Export issues'}
- ${'merge-requests'} | ${'Export merge requests'}
- `('with the issuableType "$issuableType"', ({ issuableType, modalTitle }) => {
+ issuableType | modalTitle | dataTrackLabel
+ ${'issue'} | ${'Export issues'} | ${'export_issues_csv'}
+ ${'merge_request'} | ${'Export merge requests'} | ${'export_merge-requests_csv'}
+ `('with the issuableType "$issuableType"', ({ issuableType, modalTitle, dataTrackLabel }) => {
beforeEach(() => {
wrapper = createComponent({ injectedProperties: { issuableType } });
});
@@ -57,9 +53,9 @@ describe('CsvExportModal', () => {
href: 'export/csv/path',
variant: 'confirm',
'data-method': 'post',
- 'data-qa-selector': `export_${issuableType}_button`,
+ 'data-qa-selector': `export_issues_button`,
'data-track-action': 'click_button',
- 'data-track-label': `export_${issuableType}_csv`,
+ 'data-track-label': dataTrackLabel,
},
});
});
diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
index 118c12d968b..a861148abb6 100644
--- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
+++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
@@ -16,7 +16,7 @@ describe('CsvImportExportButtons', () => {
glModalDirective = jest.fn();
return mountExtended(CsvImportExportButtons, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
glModal: {
bind(_, { value }) {
glModalDirective(value);
@@ -33,10 +33,6 @@ describe('CsvImportExportButtons', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findExportCsvButton = () => wrapper.findComponent(GlButton);
const findImportDropdown = () => wrapper.findComponent(GlDropdown);
const findImportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Import CSV' });
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index 6e954c91f46..9069d2b3ab3 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -32,10 +32,6 @@ describe('CsvImportModal', () => {
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.find('form');
const findFileInput = () => wrapper.findByLabelText('Upload CSV file');
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
index b04a6c0b8fd..4cc5775b54e 100644
--- a/spec/frontend/issuable/components/issuable_by_email_spec.js
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -53,8 +53,6 @@ describe('IssuableByEmail', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
index 99aa6778e1e..ff772040d22 100644
--- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js
+++ b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
@@ -25,16 +25,11 @@ describe('IssuableHeaderWarnings', () => {
store,
provide,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
issuableType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
diff --git a/spec/frontend/issuable/components/issue_assignees_spec.js b/spec/frontend/issuable/components/issue_assignees_spec.js
index 9a33bfae240..8ed51120508 100644
--- a/spec/frontend/issuable/components/issue_assignees_spec.js
+++ b/spec/frontend/issuable/components/issue_assignees_spec.js
@@ -21,11 +21,6 @@ describe('IssueAssigneesComponent', () => {
vm = wrapper.vm;
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
const findAvatars = () => wrapper.findAllComponents(UserAvatarLink);
const findOverflowCounter = () => wrapper.find('.avatar-counter');
diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js
index eac53c5f761..232d6177862 100644
--- a/spec/frontend/issuable/components/issue_milestone_spec.js
+++ b/spec/frontend/issuable/components/issue_milestone_spec.js
@@ -1,160 +1,61 @@
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-
import { mockMilestone } from 'jest/boards/mock_data';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return shallowMount(Component, {
- propsData: {
- milestone,
- },
- });
-};
-
-describe('IssueMilestoneComponent', () => {
+describe('IssueMilestone component', () => {
let wrapper;
- let vm;
- beforeEach(async () => {
- wrapper = createComponent();
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
- ({ vm } = wrapper);
+ const createComponent = (milestone = mockMilestone) =>
+ shallowMount(IssueMilestone, { propsData: { milestone } });
- await nextTick();
+ beforeEach(() => {
+ wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
+ it('renders milestone icon', () => {
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
});
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.isMilestoneStarted).toBe(false);
- });
-
- it('should return `true` when milestone start date is past current date', async () => {
- await wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '1990-07-22' },
- });
- await nextTick();
+ it('renders milestone title', () => {
+ expect(wrapper.find('.milestone-title').text()).toBe(mockMilestone.title);
+ });
- expect(wrapper.vm.isMilestoneStarted).toBe(true);
- });
+ describe('tooltip', () => {
+ it('renders `Milestone`', () => {
+ expect(findTooltip().text()).toContain('Milestone');
});
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.isMilestonePastDue).toBe(false);
- });
-
- it('should return `true` when milestone due is past current date', () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: '1990-07-22' },
- });
-
- expect(wrapper.vm.isMilestonePastDue).toBe(true);
- });
+ it('renders milestone title', () => {
+ expect(findTooltip().text()).toContain(mockMilestone.title);
});
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
+ describe('humanized dates', () => {
+ it('renders `Expired` when there is a due date in the past', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' });
- it('returns string containing absolute milestone start date when due date is not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)');
});
- it('returns empty string when both milestone start and due dates are not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '', due_date: '' },
- });
- await nextTick();
+ it('renders `remaining` when there is a due date in the future', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' });
- expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)');
});
- });
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
- });
- await nextTick();
+ it('renders `Started` when there is a start date in the past', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' });
- expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)');
});
- it('returns string containing milestone start date when date has already started and due date is not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
- });
- await nextTick();
+ it('renders `Starts` when there is a start date in the future', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' });
- expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)');
});
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', async () => {
- wrapper.setProps({
- milestone: {
- ...mockMilestone,
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- },
- });
- await nextTick();
-
- expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
- });
-
- it('returns empty string when milestone start and due dates are not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '', due_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.milestoneDatesHuman).toBe('');
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
});
});
});
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 3f9f048605a..3e23558ceb4 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -53,10 +53,6 @@ describe('RelatedIssuableItem', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains issuable-info-container class when canReorder is false', () => {
mountComponent({ props: { canReorder: false } });
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index 728b8958b9b..d26f287d90c 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -11,11 +11,6 @@ function factory(propsData) {
describe('Merge request status box component', () => {
const findBadge = () => wrapper.findComponent(GlBadge);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon
${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'}
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index 444165f61c7..a7605016039 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -33,10 +33,6 @@ describe('Issue Popover', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows skeleton-loader while apollo is loading', () => {
mountComponent();
diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js
index 5fdd1e6e8fc..d9e113eeaae 100644
--- a/spec/frontend/issuable/popover/components/mr_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js
@@ -71,10 +71,6 @@ describe('MR Popover', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows skeleton-loader while apollo is loading', () => {
mountComponent();
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index 72fcab63ba7..f8e47bc0a4b 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,9 +1,9 @@
-import { GlFormGroup } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import IssueToken from '~/related_issues/components/issue_token.vue';
+import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
const issuable1 = {
@@ -26,71 +26,60 @@ const issuable2 = {
const pathIdSeparator = PathIdSeparator.Issue;
-const findFormInput = (wrapper) => wrapper.find('input').element;
-
-const findRadioInput = (inputs, value) =>
- inputs.filter((input) => input.element.value === value)[0];
-
-const findRadioInputs = (wrapper) => wrapper.findAll('[name="linked-issue-type-radio"]');
-
-const constructWrapper = (props) => {
- return shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- pendingReferences: [],
- pathIdSeparator,
- ...props,
- },
- });
-};
-
describe('AddIssuableForm', () => {
let wrapper;
- afterEach(() => {
- // Jest doesn't blur an item even if it is destroyed,
- // so blur the input manually after each test
- const input = findFormInput(wrapper);
- if (input) input.blur();
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ pendingReferences: [],
+ pathIdSeparator,
+ ...props,
+ },
+ stubs: {
+ RelatedIssuableInput,
+ },
+ });
+ };
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
+ const findAddIssuableForm = () => wrapper.find('form');
+ const findFormInput = () => wrapper.find('input').element;
+ const findRadioInput = (inputs, value) =>
+ inputs.filter((input) => input.element.value === value)[0];
+ const findAllIssueTokens = () => wrapper.findAllComponents(IssueToken);
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findRadioInputs = () => wrapper.findAllComponents(GlFormRadio);
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findFormButtons = () => wrapper.findAllComponents(GlButton);
+ const findSubmitButton = () => findFormButtons().at(0);
+ const findRelatedIssuableInput = () => wrapper.findComponent(RelatedIssuableInput);
describe('with data', () => {
describe('without references', () => {
describe('without any input text', () => {
beforeEach(() => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- pendingReferences: [],
- pathIdSeparator,
- },
- });
+ createComponent();
});
it('should have disabled submit button', () => {
- expect(wrapper.vm.$refs.addButton.disabled).toBe(true);
- expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ expect(findSubmitButton().props('loading')).toBe(false);
});
});
describe('with input text', () => {
beforeEach(() => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: 'foo',
- pendingReferences: [],
- pathIdSeparator,
- },
+ createComponent({
+ inputValue: 'foo',
+ pendingReferences: [],
+ pathIdSeparator,
});
});
it('should not have disabled submit button', () => {
- expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
+ expect(findSubmitButton().props('disabled')).toBe(false);
});
});
});
@@ -99,59 +88,56 @@ describe('AddIssuableForm', () => {
const inputValue = 'foo #123';
beforeEach(() => {
- wrapper = mount(AddIssuableForm, {
- propsData: {
- inputValue,
- pendingReferences: [issuable1.reference, issuable2.reference],
- pathIdSeparator,
- },
+ createComponent({
+ inputValue,
+ pendingReferences: [issuable1.reference, issuable2.reference],
+ pathIdSeparator,
});
- });
+ }, mount);
it('should put input value in place', () => {
expect(findFormInput(wrapper).value).toBe(inputValue);
});
it('should render pending issuables items', () => {
- expect(wrapper.findAllComponents(IssueToken)).toHaveLength(2);
+ expect(findAllIssueTokens()).toHaveLength(2);
});
it('should not have disabled submit button', () => {
- expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
+ expect(findSubmitButton().props('disabled')).toBe(false);
});
});
describe('when issuable type is "issue"', () => {
beforeEach(() => {
- wrapper = mount(AddIssuableForm, {
- propsData: {
+ createComponent(
+ {
inputValue: '',
issuableType: TYPE_ISSUE,
pathIdSeparator,
pendingReferences: [],
},
- });
+ mount,
+ );
});
it('does not show radio inputs', () => {
- expect(findRadioInputs(wrapper).length).toBe(0);
+ expect(findRadioInputs()).toHaveLength(0);
});
});
describe('when issuable type is "epic"', () => {
beforeEach(() => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- issuableType: TYPE_EPIC,
- pathIdSeparator,
- pendingReferences: [],
- },
+ createComponent({
+ inputValue: '',
+ issuableType: TYPE_EPIC,
+ pathIdSeparator,
+ pendingReferences: [],
});
});
it('does not show radio inputs', () => {
- expect(findRadioInputs(wrapper).length).toBe(0);
+ expect(findRadioInputs()).toHaveLength(0);
});
});
@@ -163,17 +149,15 @@ describe('AddIssuableForm', () => {
`(
'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
({ issuableType, contextHeader, contextFooter }) => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- issuableType,
- inputValue: '',
- showCategorizedIssues: true,
- pathIdSeparator,
- pendingReferences: [],
- },
+ createComponent({
+ issuableType,
+ inputValue: '',
+ showCategorizedIssues: true,
+ pathIdSeparator,
+ pendingReferences: [],
});
- expect(wrapper.findComponent(GlFormGroup).attributes('label')).toBe(contextHeader);
+ expect(findFormGroup().attributes('label')).toBe(contextHeader);
expect(wrapper.find('p.bold').text()).toContain(contextFooter);
},
);
@@ -181,26 +165,24 @@ describe('AddIssuableForm', () => {
describe('when it is a Linked Issues form', () => {
beforeEach(() => {
- wrapper = mount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- showCategorizedIssues: true,
- issuableType: TYPE_ISSUE,
- pathIdSeparator,
- pendingReferences: [],
- },
+ createComponent({
+ inputValue: '',
+ showCategorizedIssues: true,
+ issuableType: TYPE_ISSUE,
+ pathIdSeparator,
+ pendingReferences: [],
});
});
it('shows radio inputs to allow categorisation of blocking issues', () => {
- expect(findRadioInputs(wrapper).length).toBeGreaterThan(0);
+ expect(findRadioGroup().props('options').length).toBeGreaterThan(0);
});
describe('form radio buttons', () => {
let radioInputs;
beforeEach(() => {
- radioInputs = findRadioInputs(wrapper);
+ radioInputs = findRadioInputs();
});
it('shows "relates to" option', () => {
@@ -216,58 +198,59 @@ describe('AddIssuableForm', () => {
});
it('shows 3 options in total', () => {
- expect(radioInputs.length).toBe(3);
+ expect(findRadioGroup().props('options')).toHaveLength(3);
});
});
describe('when the form is submitted', () => {
- it('emits an event with a "relates_to" link type when the "relates to" radio input selected', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO;
- wrapper.vm.onFormSubmit();
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.RELATES_TO,
- });
+ it('emits an event with a "relates_to" link type when the "relates to" radio input selected', () => {
+ findAddIssuableForm().trigger('submit');
+
+ expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([
+ [
+ {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.RELATES_TO,
+ },
+ ],
+ ]);
});
- it('emits an event with a "blocks" link type when the "blocks" radio input selected', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS;
- wrapper.vm.onFormSubmit();
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.BLOCKS,
- });
+ it('emits an event with a "blocks" link type when the "blocks" radio input selected', () => {
+ findRadioGroup().vm.$emit('input', linkedIssueTypesMap.BLOCKS);
+ findAddIssuableForm().trigger('submit');
+
+ expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([
+ [
+ {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.BLOCKS,
+ },
+ ],
+ ]);
});
it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY;
- wrapper.vm.onFormSubmit();
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
- });
+ findRadioGroup().vm.$emit('input', linkedIssueTypesMap.IS_BLOCKED_BY);
+ findAddIssuableForm().trigger('submit');
+
+ expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([
+ [
+ {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
+ },
+ ],
+ ]);
});
- it('shows error message when error is present', async () => {
+ it('shows error message when error is present', () => {
const itemAddFailureMessage = 'Something went wrong while submitting.';
- wrapper.setProps({
+ createComponent({
hasError: true,
itemAddFailureMessage,
});
- await nextTick();
expect(wrapper.find('.gl-field-error').exists()).toBe(true);
expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage);
});
@@ -283,27 +266,31 @@ describe('AddIssuableForm', () => {
};
it('returns autocomplete object', () => {
- wrapper = constructWrapper({
+ createComponent({
autoCompleteSources,
});
- expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources);
+ expect(findRelatedIssuableInput().props('autoCompleteSources')).toEqual(
+ autoCompleteSources,
+ );
- wrapper = constructWrapper({
+ createComponent({
autoCompleteSources,
confidential: false,
});
- expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources);
+ expect(findRelatedIssuableInput().props('autoCompleteSources')).toEqual(
+ autoCompleteSources,
+ );
});
it('returns autocomplete sources with query `confidential_only`, when it is confidential', () => {
- wrapper = constructWrapper({
+ createComponent({
autoCompleteSources,
confidential: true,
});
- const actualSources = wrapper.vm.transformedAutocompleteSources;
+ const actualSources = findRelatedIssuableInput().props('autoCompleteSources');
expect(actualSources.epics).toContain('?confidential_only=true');
expect(actualSources.issues).toContain('?confidential_only=true');
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 ff8d5073005..b9580b90c12 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
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlCard } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
issuable1,
@@ -78,6 +78,9 @@ describe('RelatedIssuesBlock', () => {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
+ stubs: {
+ GlCard,
+ },
slots: { 'header-text': headerText },
});
@@ -94,6 +97,9 @@ describe('RelatedIssuesBlock', () => {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
+ stubs: {
+ GlCard,
+ },
slots: { 'header-actions': headerActions },
});
@@ -222,6 +228,9 @@ describe('RelatedIssuesBlock', () => {
pathIdSeparator: PathIdSeparator.Issue,
issuableType,
},
+ stubs: {
+ GlCard,
+ },
});
const iconComponent = wrapper.findComponent(GlIcon);
@@ -239,6 +248,9 @@ describe('RelatedIssuesBlock', () => {
relatedIssues: [issuable1, issuable2, issuable3],
issuableType: TYPE_ISSUE,
},
+ stubs: {
+ GlCard,
+ },
});
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index 96c0b87e2cb..1383013aedb 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -7,7 +7,7 @@ import {
issuable1,
issuable2,
} from 'jest/issuable/components/related_issuable_mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
HTTP_STATUS_CONFLICT,
@@ -19,7 +19,7 @@ import RelatedIssuesBlock from '~/related_issues/components/related_issues_block
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/related_issues/services/related_issues_service';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RelatedIssuesRoot', () => {
let wrapper;
@@ -34,7 +34,6 @@ describe('RelatedIssuesRoot', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const createComponent = ({ props = {}, data = {} } = {}) => {
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index cc2ee84348a..21ae844e2dd 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -65,6 +65,14 @@ describe('CreateMergeRequestDropdown', () => {
expect(dropdown.createMrPath).toBe(
`${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`,
);
+
+ expect(dropdown.wrapperEl.dataset.createBranchPath).toBe(
+ `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`,
+ );
+
+ expect(dropdown.wrapperEl.dataset.createMrPath).toBe(
+ `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`,
+ );
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 77d5a0579a4..ebf4771e97f 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -18,6 +18,7 @@ import {
setSortPreferenceMutationResponse,
setSortPreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
+import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
import getIssuesCountsQuery from '~/issues/dashboard/queries/get_issues_counts.query.graphql';
import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
@@ -36,7 +37,6 @@ import {
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { IssuableStates } from '~/vue_shared/issuable/list/constants';
import {
emptyIssuesQueryResponse,
issuesCountsQueryResponse,
@@ -124,7 +124,7 @@ describe('IssuesDashboardApp component', () => {
// eslint-disable-next-line jest/no-disabled-tests
it.skip('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
- currentTab: IssuableStates.Opened,
+ currentTab: STATUS_OPEN,
hasNextPage: true,
hasPreviousPage: false,
hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
@@ -148,7 +148,7 @@ describe('IssuesDashboardApp component', () => {
tabs: IssuesDashboardApp.IssuableListTabs,
urlParams: {
sort: urlSortParams[CREATED_DESC],
- state: IssuableStates.Opened,
+ state: STATUS_OPEN,
},
useKeysetPagination: true,
});
@@ -283,7 +283,7 @@ describe('IssuesDashboardApp component', () => {
describe('state', () => {
it('is set from the url params', () => {
- const initialState = IssuableStates.All;
+ const initialState = STATUS_ALL;
setWindowLocation(`?state=${initialState}`);
mountComponent();
@@ -337,11 +337,9 @@ describe('IssuesDashboardApp component', () => {
username: 'root',
avatar_url: 'avatar/url',
};
- const originalGon = window.gon;
beforeEach(() => {
window.gon = {
- ...originalGon,
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
current_username: mockCurrentUser.username,
@@ -350,10 +348,6 @@ describe('IssuesDashboardApp component', () => {
mountComponent();
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('renders all tokens alphabetically', () => {
const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }];
@@ -375,16 +369,16 @@ describe('IssuesDashboardApp component', () => {
beforeEach(() => {
mountComponent();
- findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
});
it('updates ui to the new tab', () => {
- expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
});
it('updates url to the new tab', () => {
expect(findIssuableList().props('urlParams')).toMatchObject({
- state: IssuableStates.Closed,
+ state: STATUS_CLOSED,
});
});
});
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 ab4d023ee39..e80ffea0591 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
@@ -45,10 +45,6 @@ describe('CE IssueCardTimeInfo component', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('milestone', () => {
it('renders', () => {
wrapper = mountComponent();
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 8281ce0ed1a..b28a08e2fce 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -15,19 +15,21 @@ import waitForPromises from 'helpers/wait_for_promises';
import {
getIssuesCountsQueryResponse,
getIssuesQueryResponse,
+ getIssuesQueryEmptyResponse,
filteredTokens,
locationSearch,
setSortPreferenceMutationResponse,
setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import { IssuableListTabs } from '~/vue_shared/issuable/list/constants';
import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
@@ -70,7 +72,7 @@ import('~/issuable');
import('~/users_select');
jest.mock('@sentry/browser');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('CE IssuesListApp component', () => {
@@ -154,7 +156,24 @@ describe('CE IssuesListApp component', () => {
router = new VueRouter({ mode: 'history' });
return mountFn(IssuesListApp, {
- apolloProvider: createMockApollo(requestHandlers),
+ apolloProvider: createMockApollo(
+ requestHandlers,
+ {},
+ {
+ typePolicies: {
+ Query: {
+ fields: {
+ project: {
+ merge: true,
+ },
+ group: {
+ merge: true,
+ },
+ },
+ },
+ },
+ },
+ ),
router,
provide: {
...defaultProvide,
@@ -174,13 +193,11 @@ describe('CE IssuesListApp component', () => {
afterEach(() => {
axiosMock.reset();
- wrapper.destroy();
});
describe('IssuableList', () => {
beforeEach(() => {
wrapper = mountComponent();
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -197,7 +214,7 @@ describe('CE IssuesListApp component', () => {
initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
tabs: IssuableListTabs,
- currentTab: IssuableStates.Opened,
+ currentTab: STATUS_OPEN,
tabCounts: {
opened: 1,
closed: 1,
@@ -247,7 +264,6 @@ describe('CE IssuesListApp component', () => {
mountFn: mount,
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -416,7 +432,7 @@ describe('CE IssuesListApp component', () => {
describe('state', () => {
it('is set from the url params', () => {
- const initialState = IssuableStates.All;
+ const initialState = STATUS_ALL;
setWindowLocation(`?state=${initialState}`);
wrapper = mountComponent();
@@ -477,7 +493,12 @@ describe('CE IssuesListApp component', () => {
describe('empty states', () => {
describe('when there are issues', () => {
beforeEach(() => {
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
+ wrapper = mountComponent({
+ provide: { hasAnyIssues: true },
+ mountFn: mount,
+ issuesQueryResponse: getIssuesQueryEmptyResponse,
+ });
+ return waitForPromises();
});
it('shows EmptyStateWithAnyIssues empty state', () => {
@@ -543,11 +564,8 @@ describe('CE IssuesListApp component', () => {
});
describe('when all tokens are available', () => {
- const originalGon = window.gon;
-
beforeEach(() => {
window.gon = {
- ...originalGon,
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
current_username: mockCurrentUser.username,
@@ -563,10 +581,6 @@ describe('CE IssuesListApp component', () => {
});
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('renders all tokens alphabetically', () => {
const preloadedUsers = [
{ ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
@@ -599,7 +613,6 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -620,20 +633,21 @@ describe('CE IssuesListApp component', () => {
describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent();
+ await waitForPromises();
router.push = jest.fn();
- findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
});
it('updates ui to the new tab', () => {
- expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
});
it('updates url to the new tab', () => {
expect(router.push).toHaveBeenCalledWith({
- query: expect.objectContaining({ state: IssuableStates.Closed }),
+ query: expect.objectContaining({ state: STATUS_CLOSED }),
});
});
});
@@ -641,19 +655,25 @@ describe('CE IssuesListApp component', () => {
describe.each`
event | params
${'next-page'} | ${{
- page_after: 'endCursor',
+ page_after: 'endcursor',
page_before: undefined,
first_page_size: 20,
last_page_size: undefined,
+ search: undefined,
+ sort: 'created_date',
+ state: 'opened',
}}
${'previous-page'} | ${{
page_after: undefined,
- page_before: 'startCursor',
+ page_before: 'startcursor',
first_page_size: undefined,
last_page_size: 20,
+ search: undefined,
+ sort: 'created_date',
+ state: 'opened',
}}
`('when "$event" event is emitted by IssuableList', ({ event, params }) => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent({
data: {
pageInfo: {
@@ -662,6 +682,7 @@ describe('CE IssuesListApp component', () => {
},
},
});
+ await waitForPromises();
router.push = jest.fn();
findIssuableList().vm.$emit(event);
@@ -735,7 +756,6 @@ describe('CE IssuesListApp component', () => {
provide: { isProject },
issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -761,7 +781,6 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
issuesQueryResponse: jest.fn().mockResolvedValue(response()),
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -793,8 +812,6 @@ describe('CE IssuesListApp component', () => {
router.push = jest.fn();
findIssuableList().vm.$emit('sort', sortKey);
- jest.runOnlyPendingTimers();
- await nextTick();
expect(router.push).toHaveBeenCalledWith({
query: expect.objectContaining({ sort: urlSortParams[sortKey] }),
@@ -914,13 +931,13 @@ describe('CE IssuesListApp component', () => {
${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false}
${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true}
${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false}
- `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => {
+ `('$description', async ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => {
const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse);
wrapper = mountComponent({
provide: { isPublicVisibilityRestricted, isSignedIn },
issuesQueryResponse: mockQuery,
});
- jest.runOnlyPendingTimers();
+ await waitForPromises();
expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers }));
});
@@ -929,7 +946,6 @@ describe('CE IssuesListApp component', () => {
describe('fetching issues', () => {
beforeEach(() => {
wrapper = mountComponent();
- jest.runOnlyPendingTimers();
});
it('fetches issue, incident, test case, and task types', () => {
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 406b1fbc1af..7bbb5a954ae 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
@@ -38,11 +38,6 @@ describe('JiraIssuesImportStatus', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when Jira import is neither in progress nor finished', () => {
beforeEach(() => {
wrapper = mountComponent();
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 1e8a81116f3..0332f68ddb6 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -101,6 +101,26 @@ export const getIssuesQueryResponse = {
},
};
+export const getIssuesQueryEmptyResponse = {
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ issues: {
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [],
+ },
+ },
+ },
+};
+
export const getIssuesCountsQueryResponse = {
data: {
project: {
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index a281ed1c989..e4ecdc6c29e 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -10,7 +10,7 @@ import {
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues/list/mock_data';
-import { PAGE_SIZE, urlSortParams } from '~/issues/list/constants';
+import { urlSortParams } from '~/issues/list/constants';
import {
convertToApiParams,
convertToSearchQuery,
@@ -22,10 +22,11 @@ import {
isSortKey,
} from '~/issues/list/utils';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants';
describe('getInitialPageParams', () => {
it('returns page params with a default page size when no arguments are given', () => {
- expect(getInitialPageParams()).toEqual({ firstPageSize: PAGE_SIZE });
+ expect(getInitialPageParams()).toEqual({ firstPageSize: DEFAULT_PAGE_SIZE });
});
it('returns page params with the given page size', () => {
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 c54a762440f..4454ef81416 100644
--- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
@@ -25,10 +25,6 @@ describe('Issue title suggestions item component', () => {
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findUserAvatar = () => wrapper.findComponent(UserAvatarImage);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title', () => {
createComponent();
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index 1cd6576967a..343bdbba301 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -1,106 +1,95 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import TitleSuggestions from '~/issues/new/components/title_suggestions.vue';
import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue';
+import getIssueSuggestionsQuery from '~/issues/new/queries/issues.query.graphql';
+import { mockIssueSuggestionResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+const MOCK_PROJECT_PATH = 'project';
+const MOCK_ISSUES_COUNT = mockIssueSuggestionResponse.data.project.issues.edges.length;
describe('Issue title suggestions component', () => {
let wrapper;
+ let mockApollo;
+
+ function createComponent({
+ search = 'search',
+ queryResponse = jest.fn().mockResolvedValue(mockIssueSuggestionResponse),
+ } = {}) {
+ mockApollo = createMockApollo([[getIssueSuggestionsQuery, queryResponse]]);
- function createComponent(search = 'search') {
wrapper = shallowMount(TitleSuggestions, {
propsData: {
search,
- projectPath: 'project',
+ projectPath: MOCK_PROJECT_PATH,
},
+ apolloProvider: mockApollo,
});
}
- beforeEach(() => {
- createComponent();
- });
+ const waitForDebounce = () => {
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ };
afterEach(() => {
- wrapper.destroy();
+ mockApollo = null;
});
it('does not render with empty search', async () => {
- wrapper.setProps({ search: '' });
+ createComponent({ search: '' });
+ await waitForDebounce();
- await nextTick();
expect(wrapper.isVisible()).toBe(false);
});
- describe('with data', () => {
- let data;
-
- beforeEach(() => {
- data = { issues: [{ id: 1 }, { id: 2 }] };
- });
-
- it('renders component', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
- expect(wrapper.findAll('li').length).toBe(data.issues.length);
- });
+ it('does not render when loading', () => {
+ createComponent();
+ expect(wrapper.isVisible()).toBe(false);
+ });
- it('does not render with empty search', async () => {
- wrapper.setProps({ search: '' });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
+ it('does not render with empty issues data', async () => {
+ const emptyIssuesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ issues: {
+ edges: [],
+ },
+ },
+ },
+ };
- await nextTick();
- expect(wrapper.isVisible()).toBe(false);
- });
+ createComponent({ queryResponse: jest.fn().mockResolvedValue(emptyIssuesResponse) });
+ await waitForDebounce();
- it('does not render when loading', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- ...data,
- loading: 1,
- });
+ expect(wrapper.isVisible()).toBe(false);
+ });
- await nextTick();
- expect(wrapper.isVisible()).toBe(false);
+ describe('with data', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForDebounce();
});
- it('does not render with empty issues data', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ issues: [] });
-
- await nextTick();
- expect(wrapper.isVisible()).toBe(false);
+ it('renders component', () => {
+ expect(wrapper.findAll('li').length).toBe(MOCK_ISSUES_COUNT);
});
- it('renders list of issues', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
- expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(2);
+ it('renders list of issues', () => {
+ expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(MOCK_ISSUES_COUNT);
});
- it('adds margin class to first item', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
+ it('adds margin class to first item', () => {
expect(wrapper.findAll('li').at(0).classes()).toContain('gl-mb-3');
});
- it('does not add margin class to last item', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
+ it('does not add margin class to last item', () => {
expect(wrapper.findAll('li').at(1).classes()).not.toContain('gl-mb-3');
});
});
diff --git a/spec/frontend/issues/new/components/type_popover_spec.js b/spec/frontend/issues/new/components/type_popover_spec.js
index fe3d5207516..1ae150797c3 100644
--- a/spec/frontend/issues/new/components/type_popover_spec.js
+++ b/spec/frontend/issues/new/components/type_popover_spec.js
@@ -8,10 +8,6 @@ describe('Issue type info popover', () => {
wrapper = shallowMount(TypePopover);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createComponent();
diff --git a/spec/frontend/issues/new/mock_data.js b/spec/frontend/issues/new/mock_data.js
index 74b569d9833..0d2a388cd86 100644
--- a/spec/frontend/issues/new/mock_data.js
+++ b/spec/frontend/issues/new/mock_data.js
@@ -26,3 +26,67 @@ export default () => ({
webUrl: `${TEST_HOST}/author`,
},
});
+
+export const mockIssueSuggestionResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/278964',
+ issues: {
+ edges: [
+ {
+ node: {
+ id: 'gid://gitlab/Issue/123725957',
+ iid: '696',
+ title: 'Remove unused MR widget extension expand success, failed, warning events',
+ confidential: false,
+ userNotesCount: 16,
+ upvotes: 0,
+ webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/696',
+ state: 'opened',
+ closedAt: null,
+ createdAt: '2023-02-15T12:29:59Z',
+ updatedAt: '2023-03-01T19:38:22Z',
+ author: {
+ id: 'gid://gitlab/User/325',
+ name: 'User Name',
+ username: 'user-name',
+ avatarUrl: '/uploads/-/system/user/avatar/325/avatar.png',
+ webUrl: 'https://gitlab.com/user-name',
+ __typename: 'UserCore',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'IssueEdge',
+ },
+ {
+ node: {
+ id: 'gid://gitlab/Issue/123',
+ iid: '391',
+ title: 'Remove unused MR widget extension expand success, failed, warning events',
+ confidential: false,
+ userNotesCount: 16,
+ upvotes: 0,
+ webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391',
+ state: 'opened',
+ closedAt: null,
+ createdAt: '2023-02-15T12:29:59Z',
+ updatedAt: '2023-03-01T19:38:22Z',
+ author: {
+ id: 'gid://gitlab/User/2080',
+ name: 'User Name',
+ username: 'user-name',
+ avatarUrl: '/uploads/-/system/user/avatar/2080/avatar.png',
+ webUrl: 'https://gitlab.com/user-name',
+ __typename: 'UserCore',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'IssueEdge',
+ },
+ ],
+ __typename: 'IssueConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
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 010c719bd84..c5507c88fd7 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
@@ -34,7 +34,6 @@ describe('RelatedMergeRequests', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
index 7339372a8d1..31c96265f8d 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RelatedMergeRequest store actions', () => {
let state;
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 9fa0ce6f93d..1006f54eeaf 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -1,11 +1,10 @@
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
IssuableStatusText,
STATUS_CLOSED,
@@ -21,29 +20,27 @@ import FormComponent from '~/issues/show/components/form.vue';
import TitleComponent from '~/issues/show/components/title.vue';
import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
import PinnedLinks from '~/issues/show/components/pinned_links.vue';
-import { POLLING_DELAY } from '~/issues/show/constants';
import eventHub from '~/issues/show/event_hub';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import {
appProps,
initialRequest,
publishedIncidentUrl,
+ putRequest,
secondRequest,
zoomMeetingUrl,
} from '../mock_data/mock_data';
-jest.mock('~/flash');
-jest.mock('~/issues/show/event_hub');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/behaviors/markdown/render_gfm');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
describe('Issuable output', () => {
- let mock;
- let realtimeRequestCount = 0;
+ let axiosMock;
let wrapper;
const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header');
@@ -57,15 +54,14 @@ describe('Issuable output', () => {
const findForm = () => wrapper.findComponent(FormComponent);
const findPinnedLinks = () => wrapper.findComponent(PinnedLinks);
- const mountComponent = (props = {}, options = {}, data = {}) => {
+ const createComponent = ({ props = {}, options = {}, data = {} } = {}) => {
wrapper = shallowMountExtended(IssuableApp, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: { ...appProps, ...props },
provide: {
fullPath: 'gitlab-org/incidents',
- iid: '19',
uploadMetricsFeatureAvailable: false,
},
stubs: {
@@ -79,6 +75,28 @@ describe('Issuable output', () => {
},
...options,
});
+
+ jest.advanceTimersToNextTimer(2);
+ return waitForPromises();
+ };
+
+ const emitHubEvent = (event) => {
+ eventHub.$emit(event);
+ return waitForPromises();
+ };
+
+ const openForm = () => {
+ return emitHubEvent('open.form');
+ };
+
+ const updateIssuable = () => {
+ return emitHubEvent('update.issuable');
+ };
+
+ const advanceToNextPoll = () => {
+ // We get new data through the HTTP request.
+ jest.advanceTimersToNextTimer();
+ return waitForPromises();
};
beforeEach(() => {
@@ -98,79 +116,100 @@ describe('Issuable output', () => {
</div>
`);
- mock = new MockAdapter(axios);
- mock
- .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
- .reply(() => {
- const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
- realtimeRequestCount += 1;
- return res;
- });
+ jest.spyOn(eventHub, '$emit');
- mountComponent();
+ axiosMock = new MockAdapter(axios);
+ const endpoint = '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes';
- jest.advanceTimersByTime(2);
+ axiosMock.onGet(endpoint).replyOnce(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[0], {
+ 'POLL-INTERVAL': '1',
+ });
+ axiosMock.onGet(endpoint).reply(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[1], {
+ 'POLL-INTERVAL': '-1',
+ });
+ axiosMock.onPut().reply(HTTP_STATUS_OK, putRequest);
});
- afterEach(() => {
- mock.restore();
- realtimeRequestCount = 0;
- wrapper.vm.poll.stop();
- wrapper.destroy();
- resetHTMLFixture();
- });
+ describe('update', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
- it('should render a title/description/edited and update title/description/edited on update', () => {
- return axios
- .waitForAll()
- .then(() => {
- expect(findTitle().props('titleText')).toContain('this is a title');
- expect(findDescription().props('descriptionText')).toContain('this is a description');
-
- expect(findEdited().exists()).toBe(true);
- expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
- expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
- expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
- })
- .then(() => {
- wrapper.vm.poll.makeRequest();
- return axios.waitForAll();
- })
- .then(() => {
- expect(findTitle().props('titleText')).toContain('2');
- expect(findDescription().props('descriptionText')).toContain('42');
-
- expect(findEdited().exists()).toBe(true);
- expect(findEdited().props('updatedByName')).toBe('Other User');
- expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
- expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
- });
- });
+ it('should render a title/description/edited and update title/description/edited on update', async () => {
+ expect(findTitle().props('titleText')).toContain(initialRequest.title_text);
+ expect(findDescription().props('descriptionText')).toContain('this is a description');
- it('shows actions if permissions are correct', async () => {
- wrapper.vm.showForm = true;
+ expect(findEdited().exists()).toBe(true);
+ expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
+ expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
+ expect(findDescription().props().lockVersion).toBe(initialRequest.lock_version);
- await nextTick();
- expect(findForm().exists()).toBe(true);
- });
+ await advanceToNextPoll();
- it('does not show actions if permissions are incorrect', async () => {
- wrapper.vm.showForm = true;
- wrapper.setProps({ canUpdate: false });
+ expect(findTitle().props('titleText')).toContain('2');
+ expect(findDescription().props('descriptionText')).toContain('42');
- await nextTick();
- expect(findForm().exists()).toBe(false);
+ expect(findEdited().exists()).toBe(true);
+ expect(findEdited().props('updatedByName')).toBe('Other User');
+ expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
+ expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
+ });
});
- it('does not update formState if form is already open', async () => {
- wrapper.vm.updateAndShowForm();
+ describe('with permissions', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
- wrapper.vm.state.titleText = 'testing 123';
+ it('shows actions on `open.form` event', async () => {
+ expect(findForm().exists()).toBe(false);
- wrapper.vm.updateAndShowForm();
+ await openForm();
- await nextTick();
- expect(wrapper.vm.store.formState.title).not.toBe('testing 123');
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('update formState if form is not open', async () => {
+ const titleValue = initialRequest.title_text;
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().props('titleText')).toBe(titleValue);
+
+ await advanceToNextPoll();
+
+ // The title component has the new data, so the state was updated
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().props('titleText')).toBe(secondRequest.title_text);
+ });
+
+ it('does not update formState if form is already open', async () => {
+ const titleValue = initialRequest.title_text;
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().props('titleText')).toBe(titleValue);
+
+ await openForm();
+
+ // Opening the form, the data has not changed
+ expect(findForm().props().formState.title).toBe(titleValue);
+
+ await advanceToNextPoll();
+
+ // We expect the prop value not to have changed after another API call
+ expect(findForm().props().formState.title).toBe(titleValue);
+ });
+ });
+
+ describe('without permissions', () => {
+ beforeEach(async () => {
+ await createComponent({ props: { canUpdate: false } });
+ });
+
+ it('does not show actions if permissions are incorrect', async () => {
+ await openForm();
+
+ expect(findForm().exists()).toBe(false);
+ });
});
describe('Pinned links propagated', () => {
@@ -178,288 +217,130 @@ describe('Issuable output', () => {
prop | value
${'zoomMeetingUrl'} | ${zoomMeetingUrl}
${'publishedIncidentUrl'} | ${publishedIncidentUrl}
- `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => {
+ `('sets the $prop correctly on underlying pinned links', async ({ prop, value }) => {
+ await createComponent();
+
expect(findPinnedLinks().props(prop)).toBe(value);
});
});
- describe('updateIssuable', () => {
+ describe('updating an issue', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
it('fetches new data after update', async () => {
- const updateStoreSpy = jest.spyOn(wrapper.vm, 'updateStoreState');
- const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData');
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: { web_url: window.location.pathname },
- });
+ await advanceToNextPoll();
- await wrapper.vm.updateIssuable();
- expect(updateStoreSpy).toHaveBeenCalled();
- expect(getDataSpy).toHaveBeenCalled();
+ await updateIssuable();
+
+ expect(axiosMock.history.put).toHaveLength(1);
+ // The call was made with the new data
+ expect(axiosMock.history.put[0].data.title).toEqual(findTitle().props().title);
});
- it('correctly updates issuable data', async () => {
- const spy = jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: { web_url: window.location.pathname },
- });
+ it('closes the form after fetching data', async () => {
+ await updateIssuable();
- await wrapper.vm.updateIssuable();
- expect(spy).toHaveBeenCalledWith(wrapper.vm.formState);
expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
});
it('does not redirect if issue has not moved', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: {
- web_url: window.location.pathname,
- confidential: wrapper.vm.isConfidential,
- },
+ axiosMock.onPut().reply(HTTP_STATUS_OK, {
+ ...putRequest,
+ confidential: appProps.isConfidential,
});
- await wrapper.vm.updateIssuable();
+ await updateIssuable();
+
expect(visitUrl).not.toHaveBeenCalled();
});
it('does not redirect if issue has not moved and user has switched tabs', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: {
- web_url: '',
- confidential: wrapper.vm.isConfidential,
- },
+ axiosMock.onPut().reply(HTTP_STATUS_OK, {
+ ...putRequest,
+ web_url: '',
+ confidential: appProps.isConfidential,
});
- await wrapper.vm.updateIssuable();
+ await updateIssuable();
+
expect(visitUrl).not.toHaveBeenCalled();
});
it('redirects if returned web_url has changed', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: {
- web_url: '/testing-issue-move',
- confidential: wrapper.vm.isConfidential,
- },
- });
-
- wrapper.vm.updateIssuable();
-
- await wrapper.vm.updateIssuable();
- expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
- });
-
- describe('shows dialog when issue has unsaved changed', () => {
- it('confirms on title change', async () => {
- wrapper.vm.showForm = true;
- wrapper.vm.state.titleText = 'title has changed';
- const e = { returnValue: null };
- wrapper.vm.handleBeforeUnloadEvent(e);
-
- await nextTick();
- expect(e.returnValue).not.toBeNull();
- });
-
- it('confirms on description change', async () => {
- wrapper.vm.showForm = true;
- wrapper.vm.state.descriptionText = 'description has changed';
- const e = { returnValue: null };
- wrapper.vm.handleBeforeUnloadEvent(e);
+ const webUrl = '/testing-issue-move';
- await nextTick();
- expect(e.returnValue).not.toBeNull();
+ axiosMock.onPut().reply(HTTP_STATUS_OK, {
+ ...putRequest,
+ web_url: webUrl,
+ confidential: appProps.isConfidential,
});
- it('does nothing when nothing has changed', async () => {
- const e = { returnValue: null };
- wrapper.vm.handleBeforeUnloadEvent(e);
+ await updateIssuable();
- await nextTick();
- expect(e.returnValue).toBeNull();
- });
+ expect(visitUrl).toHaveBeenCalledWith(webUrl);
});
describe('error when updating', () => {
- it('closes form on error', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue();
+ it('closes form', async () => {
+ axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED);
- await wrapper.vm.updateIssuable();
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(createAlert).toHaveBeenCalledWith({ message: `Error updating issue` });
- });
+ await updateIssuable();
- it('returns the correct error message for issuableType', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue();
- wrapper.setProps({ issuableType: 'merge request' });
-
- await nextTick();
- await wrapper.vm.updateIssuable();
expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(createAlert).toHaveBeenCalledWith({ message: `Error updating merge request` });
- });
-
- it('shows error message from backend if exists', async () => {
- const msg = 'Custom error message from backend';
- jest
- .spyOn(wrapper.vm.service, 'updateIssuable')
- .mockRejectedValue({ response: { data: { errors: [msg] } } });
-
- await wrapper.vm.updateIssuable();
expect(createAlert).toHaveBeenCalledWith({
- message: `${wrapper.vm.defaultErrorMessage}. ${msg}`,
+ message: `Error updating issue. Request failed with status code 401`,
});
});
- });
- });
-
- describe('updateAndShowForm', () => {
- it('shows locked warning if form is open & data is different', async () => {
- await nextTick();
- wrapper.vm.updateAndShowForm();
-
- wrapper.vm.poll.makeRequest();
- await new Promise((resolve) => {
- wrapper.vm.$watch('formState.lockedWarningVisible', (value) => {
- if (value) {
- resolve();
- }
- });
- });
-
- expect(wrapper.vm.formState.lockedWarningVisible).toBe(true);
- expect(wrapper.vm.formState.lock_version).toBe(1);
- });
- });
-
- describe('requestTemplatesAndShowForm', () => {
- let formSpy;
-
- beforeEach(() => {
- formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm');
- });
-
- it('shows the form if template names as hash request is successful', () => {
- const mockData = {
- test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
- };
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
-
- return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(formSpy).toHaveBeenCalledWith(mockData);
- });
- });
+ it('returns the correct error message for issuableType', async () => {
+ axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED);
- it('shows the form if template names as array request is successful', () => {
- const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
+ await updateIssuable();
- return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(formSpy).toHaveBeenCalledWith(mockData);
- });
- });
-
- it('shows the form if template names request failed', () => {
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.reject(new Error('something went wrong')));
+ wrapper.setProps({ issuableType: 'merge request' });
- return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(createAlert).toHaveBeenCalledWith({ message: 'Error updating issue' });
+ await updateIssuable();
- expect(formSpy).toHaveBeenCalledWith();
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Error updating merge request. Request failed with status code 401`,
+ });
});
- });
- });
-
- describe('show inline edit button', () => {
- it('should render by default', () => {
- expect(findTitle().props('showInlineEditButton')).toBe(true);
- });
-
- it('should render if showInlineEditButton', async () => {
- wrapper.setProps({ showInlineEditButton: true });
-
- await nextTick();
- expect(findTitle().props('showInlineEditButton')).toBe(true);
- });
- it('should not render if showInlineEditButton is false', async () => {
- wrapper.setProps({ showInlineEditButton: false });
-
- await nextTick();
- expect(findTitle().props('showInlineEditButton')).toBe(false);
- });
- });
-
- describe('updateStoreState', () => {
- it('should make a request and update the state of the store', () => {
- const data = { foo: 1 };
- const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData').mockResolvedValue({ data });
- const updateStateSpy = jest
- .spyOn(wrapper.vm.store, 'updateState')
- .mockImplementation(jest.fn);
-
- return wrapper.vm.updateStoreState().then(() => {
- expect(getDataSpy).toHaveBeenCalled();
- expect(updateStateSpy).toHaveBeenCalledWith(data);
- });
- });
+ it('shows error message from backend if exists', async () => {
+ const msg = 'Custom error message from backend';
+ axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED, { errors: [msg] });
- it('should show error message if store update fails', () => {
- jest.spyOn(wrapper.vm.service, 'getData').mockRejectedValue();
- wrapper.setProps({ issuableType: 'merge request' });
+ await updateIssuable();
- return wrapper.vm.updateStoreState().then(() => {
expect(createAlert).toHaveBeenCalledWith({
- message: `Error updating ${wrapper.vm.issuableType}`,
+ message: `Error updating issue. ${msg}`,
});
});
});
});
- describe('issueChanged', () => {
- beforeEach(() => {
- wrapper.vm.store.formState.title = '';
- wrapper.vm.store.formState.description = '';
- wrapper.setProps({
- initialDescriptionText: '',
- initialTitleText: '',
- });
- });
-
- it('returns true when title is changed', () => {
- wrapper.vm.store.formState.title = 'RandomText';
-
- expect(wrapper.vm.issueChanged).toBe(true);
- });
-
- it('returns false when title is empty null', () => {
- wrapper.vm.store.formState.title = null;
-
- expect(wrapper.vm.issueChanged).toBe(false);
- });
-
- it('returns true when description is changed', () => {
- wrapper.vm.store.formState.description = 'RandomText';
-
- expect(wrapper.vm.issueChanged).toBe(true);
+ describe('Locked warning', () => {
+ beforeEach(async () => {
+ await createComponent();
});
- it('returns false when description is empty null', () => {
- wrapper.vm.store.formState.description = null;
-
- expect(wrapper.vm.issueChanged).toBe(false);
- });
-
- it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
- wrapper.vm.store.formState.description = '';
- wrapper.setProps({ initialDescriptionText: null });
+ it('shows locked warning if form is open & data is different', async () => {
+ await openForm();
+ await advanceToNextPoll();
- expect(wrapper.vm.issueChanged).toBe(false);
+ expect(findForm().props().formState.lockedWarningVisible).toBe(true);
+ expect(findForm().props().formState.lock_version).toBe(1);
});
});
describe('sticky header', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
describe('when title is in view', () => {
it('is not shown', () => {
expect(findStickyHeader().exists()).toBe(false);
@@ -467,21 +348,18 @@ describe('Issuable output', () => {
});
describe('when title is not in view', () => {
- beforeEach(() => {
- wrapper.vm.state.titleText = 'Sticky header title';
+ beforeEach(async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
});
it('shows with title', () => {
- expect(findStickyHeader().text()).toContain('Sticky header title');
+ expect(findStickyHeader().text()).toContain(initialRequest.title_text);
});
it('shows with title for an epic', async () => {
- wrapper.setProps({ issuableType: 'epic' });
-
- await nextTick();
+ await wrapper.setProps({ issuableType: 'epic' });
- expect(findStickyHeader().text()).toContain('Sticky header title');
+ expect(findStickyHeader().text()).toContain(' this is a title');
});
it.each`
@@ -493,9 +371,7 @@ describe('Issuable output', () => {
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
async ({ issuableType, issuableStatus, statusIcon }) => {
- wrapper.setProps({ issuableType, issuableStatus });
-
- await nextTick();
+ await wrapper.setProps({ issuableType, issuableStatus });
expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon);
},
@@ -507,9 +383,7 @@ describe('Issuable output', () => {
${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
`('$title', async ({ state }) => {
- wrapper.setProps({ issuableStatus: state });
-
- await nextTick();
+ await wrapper.setProps({ issuableStatus: state });
expect(findStickyHeader().text()).toContain(IssuableStatusText[state]);
});
@@ -519,9 +393,7 @@ describe('Issuable output', () => {
${'does not show confidential badge when issue is not confidential'} | ${false}
${'shows confidential badge when issue is confidential'} | ${true}
`('$title', async ({ isConfidential }) => {
- wrapper.setProps({ isConfidential });
-
- await nextTick();
+ await wrapper.setProps({ isConfidential });
const confidentialEl = findConfidentialBadge();
expect(confidentialEl.exists()).toBe(isConfidential);
@@ -538,9 +410,7 @@ describe('Issuable output', () => {
${'does not show locked badge when issue is not locked'} | ${false}
${'shows locked badge when issue is locked'} | ${true}
`('$title', async ({ isLocked }) => {
- wrapper.setProps({ isLocked });
-
- await nextTick();
+ await wrapper.setProps({ isLocked });
expect(findLockedBadge().exists()).toBe(isLocked);
});
@@ -550,9 +420,7 @@ describe('Issuable output', () => {
${'does not show hidden badge when issue is not hidden'} | ${false}
${'shows hidden badge when issue is hidden'} | ${true}
`('$title', async ({ isHidden }) => {
- wrapper.setProps({ isHidden });
-
- await nextTick();
+ await wrapper.setProps({ isHidden });
const hiddenBadge = findHiddenBadge();
@@ -569,6 +437,10 @@ describe('Issuable output', () => {
});
describe('Composable description component', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
const findIncidentTabs = () => wrapper.findComponent(IncidentTabs);
const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6';
@@ -587,13 +459,13 @@ describe('Issuable output', () => {
});
describe('when using incident tabs description wrapper', () => {
- beforeEach(() => {
- mountComponent(
- {
+ beforeEach(async () => {
+ await createComponent({
+ props: {
descriptionComponent: IncidentTabs,
showTitleBorder: false,
},
- {
+ options: {
mocks: {
$apollo: {
queries: {
@@ -604,7 +476,7 @@ describe('Issuable output', () => {
},
},
},
- );
+ });
});
it('does not the description component', () => {
@@ -622,48 +494,77 @@ describe('Issuable output', () => {
});
describe('taskListUpdateStarted', () => {
- it('stops polling', () => {
- jest.spyOn(wrapper.vm.poll, 'stop');
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('stops polling', async () => {
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
+
+ findDescription().vm.$emit('taskListUpdateStarted');
- wrapper.vm.taskListUpdateStarted();
+ await advanceToNextPoll();
- expect(wrapper.vm.poll.stop).toHaveBeenCalled();
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
});
});
describe('taskListUpdateSucceeded', () => {
- it('enables polling', () => {
- jest.spyOn(wrapper.vm.poll, 'enable');
- jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
+ beforeEach(async () => {
+ await createComponent();
+ findDescription().vm.$emit('taskListUpdateStarted');
+ });
+
+ it('enables polling', async () => {
+ // Ensure that polling is not working before
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
+ await advanceToNextPoll();
+
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
- wrapper.vm.taskListUpdateSucceeded();
+ // Enable Polling an move forward
+ findDescription().vm.$emit('taskListUpdateSucceeded');
+ await advanceToNextPoll();
- expect(wrapper.vm.poll.enable).toHaveBeenCalled();
- expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
+ // Title has changed: polling works!
+ expect(findTitle().props().titleText).toBe(secondRequest.title_text);
});
});
describe('taskListUpdateFailed', () => {
- it('enables polling and calls updateStoreState', () => {
- jest.spyOn(wrapper.vm.poll, 'enable');
- jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
- jest.spyOn(wrapper.vm, 'updateStoreState');
+ beforeEach(async () => {
+ await createComponent();
+ findDescription().vm.$emit('taskListUpdateStarted');
+ });
+
+ it('enables polling and calls updateStoreState', async () => {
+ // Ensure that polling is not working before
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
+ await advanceToNextPoll();
- wrapper.vm.taskListUpdateFailed();
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
- expect(wrapper.vm.poll.enable).toHaveBeenCalled();
- expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
- expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
+ // Enable Polling an move forward
+ findDescription().vm.$emit('taskListUpdateFailed');
+ await advanceToNextPoll();
+
+ // Title has changed: polling works!
+ expect(findTitle().props().titleText).toBe(secondRequest.title_text);
});
});
describe('saveDescription event', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
it('makes request to update issue', async () => {
const description = 'I have been updated!';
findDescription().vm.$emit('saveDescription', description);
+
await waitForPromises();
- expect(mock.history.put[0].data).toContain(description);
+ expect(axiosMock.history.put[0].data).toContain(description);
});
});
});
diff --git a/spec/frontend/issues/show/components/delete_issue_modal_spec.js b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
index 97a091a1748..b8adeb24005 100644
--- a/spec/frontend/issues/show/components/delete_issue_modal_spec.js
+++ b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
@@ -20,10 +20,6 @@ describe('DeleteIssueModal component', () => {
const mountComponent = (props = {}) =>
shallowMount(DeleteIssueModal, { propsData: { ...defaultProps, ...props } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('modal', () => {
it('renders', () => {
wrapper = mountComponent();
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index da51372dd3d..740b2f782e4 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,25 +1,19 @@
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlModal } from '@gitlab/ui';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
-import { mockTracking } from 'helpers/tracking_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
-import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import TaskList from '~/task_list';
-import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
createWorkItemMutationErrorResponse,
@@ -27,14 +21,9 @@ import {
getIssueDetailsResponse,
projectWorkItemTypesQueryResponse,
} from 'jest/work_items/mock_data';
-import {
- descriptionProps as initialProps,
- descriptionHtmlWithList,
- descriptionHtmlWithCheckboxes,
- descriptionHtmlWithTask,
-} from '../mock_data/mock_data';
+import { descriptionProps as initialProps, descriptionHtmlWithList } from '../mock_data/mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -43,9 +32,6 @@ jest.mock('~/task_list');
jest.mock('~/behaviors/markdown/render_gfm');
const mockSpriteIcons = '/icons.svg';
-const showModal = jest.fn();
-const hideModal = jest.fn();
-const showDetailsModal = jest.fn();
const $toast = {
show: jest.fn(),
};
@@ -62,7 +48,6 @@ const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTyp
describe('Description component', () => {
let wrapper;
- let originalGon;
Vue.use(VueApollo);
@@ -70,21 +55,16 @@ describe('Description component', () => {
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
const findListItems = () => findGfmContent().findAll('ul > li');
const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions');
- const findTaskLink = () => wrapper.find('a.gfm-issue');
- const findModal = () => wrapper.findComponent(GlModal);
- const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({
props = {},
provide,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse),
createWorkItemMutationHandler,
- ...options
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
- issueIid: 1,
...initialProps,
...props,
},
@@ -102,25 +82,10 @@ describe('Description component', () => {
mocks: {
$toast,
},
- stubs: {
- GlModal: stubComponent(GlModal, {
- methods: {
- show: showModal,
- hide: hideModal,
- },
- }),
- WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
- methods: {
- show: showDetailsModal,
- },
- }),
- },
- ...options,
});
}
beforeEach(() => {
- originalGon = window.gon;
window.gon = { sprite_icons: mockSpriteIcons };
setWindowLocation(TEST_HOST);
@@ -136,8 +101,6 @@ describe('Description component', () => {
});
afterAll(() => {
- window.gon = originalGon;
-
$('.issuable-meta .flash-container').remove();
});
@@ -285,7 +248,6 @@ describe('Description component', () => {
props: {
descriptionHtml: descriptionHtmlWithList,
},
- attachTo: document.body,
});
await nextTick();
});
@@ -325,33 +287,6 @@ describe('Description component', () => {
});
});
- describe('description with checkboxes', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- });
- return nextTick();
- });
-
- it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
- expect(findTaskActionButtons()).toHaveLength(3);
- });
-
- it('does not show a modal by default', () => {
- 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('task list item actions', () => {
describe('converting the task list item to a task', () => {
describe('when successful', () => {
@@ -391,11 +326,7 @@ describe('Description component', () => {
});
it('calls a mutation to create a task', () => {
- const {
- confidential,
- iteration,
- milestone,
- } = issueDetailsResponse.data.workspace.issuable;
+ const { confidential, iteration, milestone } = issueDetailsResponse.data.issue;
expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
input: {
confidential,
@@ -468,109 +399,4 @@ describe('Description component', () => {
});
});
});
-
- describe('work items detail', () => {
- describe('when opening and closing', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- });
- return nextTick();
- });
-
- it('opens when task button is clicked', async () => {
- await findTaskLink().trigger('click');
-
- expect(showDetailsModal).toHaveBeenCalled();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/?work_item_id=2`,
- replace: true,
- });
- });
-
- it('closes from an open state', async () => {
- await findTaskLink().trigger('click');
-
- findWorkItemDetailModal().vm.$emit('close');
- await nextTick();
-
- expect(updateHistory).toHaveBeenLastCalledWith({
- url: `${TEST_HOST}/`,
- replace: true,
- });
- });
-
- it('tracks when opened', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- await findTaskLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'viewed_work_item_from_modal',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'work_item_view',
- property: 'type_task',
- },
- );
- });
- });
-
- describe('when url query `work_item_id` exists', () => {
- it.each`
- behavior | workItemId | modalOpened
- ${'opens'} | ${'2'} | ${1}
- ${'does not open'} | ${'123'} | ${0}
- ${'does not open'} | ${'123e'} | ${0}
- ${'does not open'} | ${'12e3'} | ${0}
- ${'does not open'} | ${'1e23'} | ${0}
- ${'does not open'} | ${'x'} | ${0}
- ${'does not open'} | ${'undefined'} | ${0}
- `(
- '$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, modalOpened }) => {
- setWindowLocation(`?work_item_id=${workItemId}`);
-
- createComponent({
- props: { descriptionHtml: descriptionHtmlWithTask },
- });
-
- expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
- },
- );
- });
- });
-
- describe('when hovering task links', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- });
- return nextTick();
- });
-
- it('prefetches work item detail after work item link is hovered for 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- jest.advanceTimersByTime(150);
- await waitForPromises();
-
- expect(queryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
- });
- });
-
- it('does not work item detail after work item link is hovered for less than 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- await findTaskLink().trigger('mouseout');
- jest.advanceTimersByTime(150);
- await waitForPromises();
-
- expect(queryHandler).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js
index 11c43ea4388..ca561149806 100644
--- a/spec/frontend/issues/show/components/edit_actions_spec.js
+++ b/spec/frontend/issues/show/components/edit_actions_spec.js
@@ -56,10 +56,6 @@ describe('Edit Actions component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all buttons as enabled', () => {
const buttons = findEditButtons().wrappers;
buttons.forEach((button) => {
diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js
index aa6e0a9dceb..a509627c347 100644
--- a/spec/frontend/issues/show/components/edited_spec.js
+++ b/spec/frontend/issues/show/components/edited_spec.js
@@ -15,10 +15,6 @@ describe('Edited component', () => {
const mountComponent = (propsData) => mount(Edited, { propsData });
const updatedAt = '2017-05-15T12:31:04.428Z';
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders an edited at+by string', () => {
wrapper = mountComponent({
updatedAt,
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 273ddfdd5d4..5c145ed4707 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -33,11 +33,6 @@ describe('Description field component', () => {
jest.spyOn(eventHub, '$emit');
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders markdown field with description', () => {
wrapper = mountComponent();
@@ -80,17 +75,18 @@ describe('Description field component', () => {
});
it('uses the MarkdownEditor component to edit markdown', () => {
- expect(findMarkdownEditor().props()).toEqual(
- expect.objectContaining({
- value: 'test',
- renderMarkdownPath: '/',
- markdownDocsPath: '/',
- quickActionsDocsPath: expect.any(String),
- autofocus: true,
- supportsQuickActions: true,
- enableAutocomplete: true,
- }),
- );
+ expect(findMarkdownEditor().props()).toMatchObject({
+ value: 'test',
+ renderMarkdownPath: '/',
+ autofocus: true,
+ supportsQuickActions: true,
+ quickActionsDocsPath: expect.any(String),
+ });
+
+ expect(findMarkdownEditor().vm.$attrs).toMatchObject({
+ 'enable-autocomplete': true,
+ 'markdown-docs-path': '/',
+ });
});
it('triggers update with meta+enter', () => {
diff --git a/spec/frontend/issues/show/components/fields/description_template_spec.js b/spec/frontend/issues/show/components/fields/description_template_spec.js
index 79a3bfa9840..1e8d5e2dd95 100644
--- a/spec/frontend/issues/show/components/fields/description_template_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_template_spec.js
@@ -22,10 +22,6 @@ describe('Issue description template component with templates as hash', () => {
wrapper = shallowMount(descriptionTemplate, options);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders templates as JSON hash in data attribute', () => {
createComponent();
expect(findIssuableSelector().attributes('data-data')).toBe(
diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js
index a5fa96d8d64..b28762f1520 100644
--- a/spec/frontend/issues/show/components/fields/title_spec.js
+++ b/spec/frontend/issues/show/components/fields/title_spec.js
@@ -17,11 +17,6 @@ describe('Title field component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders form control with formState title', () => {
expect(findInput().element.value).toBe('test');
});
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 27ac0e1baf3..e655cf3b37d 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -1,4 +1,4 @@
-import { GlFormGroup, GlListbox, GlIcon } from '@gitlab/ui';
+import { GlFormGroup, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -32,7 +32,7 @@ describe('Issue type field component', () => {
},
};
- const findListBox = () => wrapper.findComponent(GlListbox);
+ const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findAllIssueItems = () => wrapper.findAll('[data-testid="issue-type-list-item"]');
const findIssueItemAt = (at) => findAllIssueItems().at(at);
@@ -60,10 +60,6 @@ describe('Issue type field component', () => {
mockIssueStateData = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
at | text | icon
${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js
index aedb974cbd0..b8ed33801f2 100644
--- a/spec/frontend/issues/show/components/form_spec.js
+++ b/spec/frontend/issues/show/components/form_spec.js
@@ -30,10 +30,6 @@ describe('Inline edit form component', () => {
projectNamespace: '/',
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = (props) => {
wrapper = shallowMount(formComponent, {
propsData: {
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 3d9dad3a721..58ec7387851 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,20 +1,22 @@
import Vue, { nextTick } from 'vue';
-import { GlButton, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
+import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { IssueType, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
+import issuesEventHub from '~/issues/show/event_hub';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
-jest.mock('~/flash');
+jest.mock('~/alert');
+jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() }));
describe('HeaderActions component', () => {
let dispatchEventSpy;
@@ -36,7 +38,7 @@ describe('HeaderActions component', () => {
iid: '32',
isIssueAuthor: true,
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
- issueType: IssueType.Issue,
+ issueType: TYPE_ISSUE,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath: '-/abuse_reports/add_category',
@@ -67,7 +69,8 @@ describe('HeaderActions component', () => {
},
};
- const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
+ const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`);
+ const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`);
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
@@ -103,6 +106,9 @@ describe('HeaderActions component', () => {
mutate: mutateMock,
},
},
+ stubs: {
+ GlButton,
+ },
});
};
@@ -113,13 +119,12 @@ describe('HeaderActions component', () => {
if (visitUrlSpy) {
visitUrlSpy.mockRestore();
}
- wrapper.destroy();
});
describe.each`
issueType
- ${IssueType.Issue}
- ${IssueType.Incident}
+ ${TYPE_ISSUE}
+ ${TYPE_INCIDENT}
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
@@ -240,6 +245,30 @@ describe('HeaderActions component', () => {
});
});
});
+
+ describe(`show edit button ${issueType}`, () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ canUpdateIssue: true,
+ canCreateIssue: false,
+ isIssueAuthor: true,
+ issueType,
+ canReportSpam: false,
+ canPromoteToEpic: false,
+ },
+ });
+ });
+ it(`shows the edit button`, () => {
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('should trigger "open.form" event when clicked', async () => {
+ expect(issuesEventHub.$emit).not.toHaveBeenCalled();
+ await findEditButton().trigger('click');
+ expect(issuesEventHub.$emit).toHaveBeenCalledWith('open.form');
+ });
+ });
});
describe('delete issue button', () => {
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 6c923cae0cc..6b68e7a0da6 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
@@ -9,7 +9,7 @@ import createTimelineEventMutation from '~/issues/show/components/incidents/grap
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 { createAlert } from '~/alert';
import { useFakeDate } from 'helpers/fake_date';
import {
timelineEventsCreateEventResponse,
@@ -19,7 +19,7 @@ import {
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
const fakeDate = '2020-07-08T00:00:00.000Z';
@@ -99,7 +99,6 @@ describe('Create Timeline events', () => {
afterEach(() => {
createAlert.mockReset();
- wrapper.destroy();
});
describe('createIncidentTimelineEvent', () => {
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 33a3a6eddfc..0f4fb02a40b 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -1,4 +1,5 @@
import merge from 'lodash/merge';
+import { nextTick } from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issues/show/components/description.vue';
@@ -11,6 +12,11 @@ import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import { descriptionProps } from '../../mock_data/mock_data';
+const push = jest.fn();
+const $router = {
+ push,
+};
+
const mockAlert = {
__typename: 'AlertManagementAlert',
detailsUrl: INVALID_URL,
@@ -28,12 +34,20 @@ const defaultMocks = {
},
},
},
+ $route: { params: {} },
+ $router,
};
describe('Incident Tabs component', () => {
let wrapper;
- const mountComponent = ({ data = {}, options = {}, mount = shallowMountExtended } = {}) => {
+ const mountComponent = ({
+ data = {},
+ options = {},
+ mount = shallowMountExtended,
+ hasLinkedAlerts = false,
+ mocks = {},
+ } = {}) => {
wrapper = mount(
IncidentTabs,
merge(
@@ -54,11 +68,12 @@ describe('Incident Tabs component', () => {
slaFeatureAvailable: true,
canUpdate: true,
canUpdateTimelineEvent: true,
+ hasLinkedAlerts,
},
data() {
return { alert: mockAlert, ...data };
},
- mocks: defaultMocks,
+ mocks: { ...defaultMocks, ...mocks },
},
options,
),
@@ -102,11 +117,13 @@ describe('Incident Tabs component', () => {
});
it('renders the alert details tab', () => {
+ mountComponent({ hasLinkedAlerts: true });
expect(findAlertDetailsTab().exists()).toBe(true);
expect(findAlertDetailsTab().attributes('title')).toBe('Alert details');
});
it('renders the alert details table with the correct props', () => {
+ mountComponent({ hasLinkedAlerts: true });
const alert = { iid: mockAlert.iid };
expect(findAlertDetailsComponent().props('alert')).toMatchObject(alert);
@@ -156,6 +173,40 @@ describe('Incident Tabs component', () => {
expect(findActiveTabs()).toHaveLength(1);
expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle);
+ expect(push).toHaveBeenCalledWith('/timeline');
+ });
+ });
+
+ describe('loading page with tab', () => {
+ it('shows the timeline tab when timeline path is passed', async () => {
+ mountComponent({
+ mount: mountExtended,
+ mocks: { $route: { params: { tabId: 'timeline' } } },
+ });
+ await nextTick();
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle);
+ });
+
+ it('shows the alerts tab when timeline path is passed', async () => {
+ mountComponent({
+ mount: mountExtended,
+ mocks: { $route: { params: { tabId: 'alerts' } } },
+ hasLinkedAlerts: true,
+ });
+ await nextTick();
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.alertsTitle);
+ });
+
+ it('shows the metrics tab when metrics path is passed', async () => {
+ mountComponent({
+ mount: mountExtended,
+ mocks: { $route: { params: { tabId: 'metrics' } } },
+ });
+ await nextTick();
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.metricsTitle);
});
});
});
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 e352f9708e4..af01fd34336 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
@@ -10,12 +10,12 @@ import {
TIMELINE_EVENT_TAGS,
timelineEventTagsI18n,
} from '~/issues/show/components/incidents/constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { useFakeDate } from 'helpers/fake_date';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
const fakeDate = '2020-07-08T00:00:00.000Z';
@@ -51,7 +51,6 @@ describe('Timeline events form', () => {
afterEach(() => {
createAlert.mockReset();
- wrapper.destroy();
});
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
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 26fda877089..8d79dece888 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
@@ -11,7 +11,7 @@ import deleteTimelineEventMutation from '~/issues/show/components/incidents/grap
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 { createAlert } from '~/alert';
import {
mockEvents,
timelineEventsDeleteEventResponse,
@@ -26,7 +26,7 @@ import {
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const mockConfirmAction = ({ confirmed }) => {
@@ -77,10 +77,6 @@ describe('IncidentTimelineEventList', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('groups items correctly', () => {
expect(findTimelineEventGroups()).toHaveLength(2);
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 63474070701..48c3f0984a0 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
@@ -8,13 +8,13 @@ import IncidentTimelineEventsList from '~/issues/show/components/incidents/timel
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { timelineTabI18n } from '~/issues/show/components/incidents/constants';
import { timelineEventsQueryListResponse, timelineEventsQueryEmptyResponse } from './mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
const graphQLError = new Error('GraphQL error');
const listResponse = jest.fn().mockResolvedValue(timelineEventsQueryListResponse);
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
index 75be17f9889..8ee0d906dd4 100644
--- a/spec/frontend/issues/show/components/incidents/utils_spec.js
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -5,10 +5,10 @@ import {
getUtcShiftedDate,
getPreviousEventTags,
} from '~/issues/show/components/incidents/utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { mockTimelineEventTags } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('incident utils', () => {
describe('display and log error', () => {
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
index dd3c7c58380..f8a8c999632 100644
--- a/spec/frontend/issues/show/components/locked_warning_spec.js
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -13,11 +13,6 @@ describe('LockedWarning component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index d52f9d57453..8caa5236796 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -17,7 +17,7 @@ describe('TaskListItemActions component', () => {
document.body.appendChild(li);
wrapper = shallowMount(TaskListItemActions, {
- provide: { canUpdate: true, toggleClass: 'task-list-item-actions' },
+ provide: { canUpdate: true },
attachTo: document.querySelector('div'),
});
};
diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js
index 7560b733ae6..16ac675e12c 100644
--- a/spec/frontend/issues/show/components/title_spec.js
+++ b/spec/frontend/issues/show/components/title_spec.js
@@ -1,96 +1,59 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import titleComponent from '~/issues/show/components/title.vue';
-import eventHub from '~/issues/show/event_hub';
-import Store from '~/issues/show/stores';
+import Title from '~/issues/show/components/title.vue';
describe('Title component', () => {
- let vm;
- beforeEach(() => {
+ let wrapper;
+
+ const getTitleHeader = () => wrapper.findByTestId('issue-title');
+
+ const createWrapper = (props) => {
setHTMLFixture(`<title />`);
- const Component = Vue.extend(titleComponent);
- const store = new Store({
- titleHtml: '',
- descriptionHtml: '',
- issuableRef: '',
- });
- vm = new Component({
+ wrapper = shallowMountExtended(Title, {
propsData: {
issuableRef: '#1',
titleHtml: 'Testing <img />',
titleText: 'Testing',
- showForm: false,
- formState: store.formState,
+ ...props,
},
- }).$mount();
- });
+ });
+ };
afterEach(() => {
resetHTMLFixture();
});
it('renders title HTML', () => {
- expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
- });
-
- it('updates page title when changing titleHtml', async () => {
- const spy = jest.spyOn(vm, 'setPageTitle');
- vm.titleHtml = 'test';
+ createWrapper();
- await nextTick();
- expect(spy).toHaveBeenCalled();
+ expect(getTitleHeader().element.innerHTML.trim()).toBe('Testing <img>');
});
it('animates title changes', async () => {
- vm.titleHtml = 'test';
+ createWrapper();
- await nextTick();
+ await wrapper.setProps({
+ titleHtml: 'test',
+ });
- expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse');
- jest.runAllTimers();
+ expect(getTitleHeader().classes('issue-realtime-pre-pulse')).toBe(true);
+ jest.runAllTimers();
await nextTick();
- expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse');
+ expect(getTitleHeader().classes('issue-realtime-trigger-pulse')).toBe(true);
});
it('updates page title after changing title', async () => {
- vm.titleHtml = 'changed';
- vm.titleText = 'changed';
-
- await nextTick();
- expect(document.querySelector('title').textContent.trim()).toContain('changed');
- });
+ createWrapper();
- describe('inline edit button', () => {
- it('should not show by default', () => {
- expect(vm.$el.querySelector('.btn-edit')).toBeNull();
+ await wrapper.setProps({
+ titleHtml: 'changed',
+ titleText: 'changed',
});
- it('should not show if canUpdate is false', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = false;
-
- expect(vm.$el.querySelector('.btn-edit')).toBeNull();
- });
-
- it('should show if showInlineEditButton and canUpdate', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = true;
-
- expect(vm.$el.querySelector('.btn-edit')).toBeDefined();
- });
-
- it('should trigger open.form event when clicked', async () => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- vm.showInlineEditButton = true;
- vm.canUpdate = true;
-
- await nextTick();
- vm.$el.querySelector('.btn-edit').click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
- });
+ expect(document.querySelector('title').textContent.trim()).toContain('changed');
});
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 9f0b6fb1148..86d09665947 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -24,6 +24,19 @@ export const secondRequest = {
lock_version: 2,
};
+export const putRequest = {
+ web_url: window.location.pathname,
+ title: '<p>PUT</p>',
+ title_text: 'PUT',
+ description: '<p>PUT_DESC</p>',
+ description_text: 'PUT_DESC',
+ task_status: '0 of 0 completed',
+ updated_at: '2016-05-15T12:31:04.428Z',
+ updated_by_name: 'Other User',
+ updated_by_path: '/other_user',
+ lock_version: 2,
+};
+
export const descriptionProps = {
canUpdate: true,
descriptionHtml: 'test',
@@ -66,47 +79,3 @@ export const descriptionHtmlWithList = `
<li data-sourcepos="3:1-3:8">todo 3</li>
</ul>
`;
-
-export const descriptionHtmlWithCheckboxes = `
- <ul dir="auto" class="task-list" data-sourcepos"3:1-5:12">
- <li class="task-list-item" data-sourcepos="3:1-3:11">
- <input class="task-list-item-checkbox" type="checkbox"> todo 1
- </li>
- <li class="task-list-item" data-sourcepos="4:1-4:12">
- <input class="task-list-item-checkbox" type="checkbox"> todo 2
- </li>
- <li class="task-list-item" data-sourcepos="5:1-5:12">
- <input class="task-list-item-checkbox" type="checkbox"> todo 3
- </li>
- </ul>
-`;
-
-export const descriptionHtmlWithTask = `
- <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
- <li data-sourcepos="1:1-1:10" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a>
- </li>
- <li data-sourcepos="2:1-2:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 2
- </li>
- <li data-sourcepos="3:1-3:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 3
- </li>
- </ul>
-`;
-
-export const descriptionHtmlWithIssue = `
- <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
- <li data-sourcepos="1:1-1:10" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a>
- </li>
- <li data-sourcepos="2:1-2:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 2
- </li>
- <li data-sourcepos="3:1-3:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 3
- </li>
- </ul>
-`;
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 d41031f9eaa..5e6b67aec40 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
@@ -78,10 +78,6 @@ describe('NewBranchForm', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when selecting items from dropdowns', () => {
describe('when no project selected', () => {
beforeEach(() => {
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index 944854faab3..0a887efee4b 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -49,10 +49,6 @@ describe('ProjectDropdown', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading projects', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index 56e425fa4eb..701512953df 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -54,10 +54,6 @@ describe('SourceBranchDropdown', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when `selectedProject` prop is not specified', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/jira_connect/branches/pages/index_spec.js b/spec/frontend/jira_connect/branches/pages/index_spec.js
index 92976dd28da..4b79d5feab5 100644
--- a/spec/frontend/jira_connect/branches/pages/index_spec.js
+++ b/spec/frontend/jira_connect/branches/pages/index_spec.js
@@ -25,10 +25,6 @@ describe('NewBranchForm', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('page title', () => {
it.each`
initialBranchName | pageTitle
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js
index e2a14a9102f..5a28c6d9789 100644
--- a/spec/frontend/jira_connect/subscriptions/api_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/api_spec.js
@@ -17,7 +17,6 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({
describe('JiraConnect API', () => {
let axiosMock;
- let originalGon;
let response;
const mockAddPath = 'addPath';
@@ -29,13 +28,11 @@ describe('JiraConnect API', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axiosInstance);
- originalGon = window.gon;
window.gon = { api_version: 'v4' };
});
afterEach(() => {
axiosMock.restore();
- window.gon = originalGon;
response = null;
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
index 9f92ad2adc1..934473c15ba 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
@@ -11,7 +11,7 @@ describe('AddNamespaceButton', () => {
const createComponent = () => {
wrapper = shallowMount(AddNamespaceButton, {
directives: {
- glModal: createMockDirective(),
+ glModal: createMockDirective('gl-modal'),
},
});
};
@@ -23,10 +23,6 @@ describe('AddNamespaceButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a button', () => {
expect(findButton().exists()).toBe(true);
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
index d80381107f2..dbe8a734bb4 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
@@ -17,10 +17,6 @@ describe('AddNamespaceModal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays modal with correct props', () => {
const modal = findModal();
expect(modal.exists()).toBe(true);
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index 5df54abfc05..e437e6e0398 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -40,10 +40,6 @@ describe('GroupsListItem', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGroupItemName = () => wrapper.findComponent(GroupItemName);
const findLinkButton = () => wrapper.findComponent(GlButton);
const clickLinkButton = () => findLinkButton().trigger('click');
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 97038a2a231..9d5bc8dff2a 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
@@ -48,10 +48,6 @@ describe('GroupsList', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAllItems = () => wrapper.findAllComponents(GroupsListItem);
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 369ddda8dbe..aa4feaa5261 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -41,10 +41,6 @@ describe('JiraConnectApp', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe.each`
scenario | usersPath | shouldRenderSignInPage | shouldRenderSubscriptionsPage
diff --git a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
index aa93a6be3c8..a8aa383d917 100644
--- a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
@@ -12,10 +12,6 @@ describe('BrowserSupportAlert', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a non-dismissible alert', () => {
createComponent();
diff --git a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js
index b5fe08486b1..e4da10569f3 100644
--- a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js
@@ -14,10 +14,6 @@ describe('GroupItemName', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('matches the snapshot', () => {
createComponent();
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
index 4ebfaed261e..0dadec598e4 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
@@ -22,10 +22,6 @@ describe('SignInLegacyButton', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a button', () => {
createComponent();
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 e20c4b62e77..9d39b82c05f 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
@@ -56,10 +56,6 @@ describe('SignInOauthButton', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.findComponent(GlButton);
describe('when `gitlabBasePath` is GitLab.com', () => {
it('displays a button', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 2d7c58fc278..5337575d5ef 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -29,10 +29,6 @@ describe('SubscriptionsList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findUnlinkButton = () => wrapper.findComponent(GlButton);
const clickUnlinkButton = () => findUnlinkButton().trigger('click');
diff --git a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
index e16121243a0..c0bd908da0f 100644
--- a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
@@ -28,10 +28,6 @@ describe('UserLink', () => {
const findSprintf = () => wrapper.findComponent(GlSprintf);
const findOauthButton = () => wrapper.findComponent(SignInOauthButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
userSignedIn | hasSubscriptions | expectGlSprintf | expectGlLink | expectOauthButton | jiraConnectOauthEnabled
${true} | ${false} | ${true} | ${false} | ${false} | ${false}
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 b9a8451f3b3..be46c1d1609 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
@@ -42,10 +42,6 @@ describe('SignInGitlabCom', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe.each`
scenario | hasSubscriptions | signInButtonText
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 e98c6ff1054..d99d8986296 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
@@ -32,10 +32,6 @@ describe('SignInGitlabMultiversion', () => {
wrapper = shallowMountExtended(SignInGitlabMultiversion);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when version is not selected', () => {
describe('VersionSelectForm', () => {
it('renders version select form', () => {
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
index 29e7fe7a5b2..428aa1d734b 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
@@ -17,10 +17,6 @@ describe('VersionSelectForm', () => {
wrapper = shallowMountExtended(VersionSelectForm);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default state', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
index b27eba6b040..7639c3a9c3f 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
@@ -34,10 +34,6 @@ describe('SignInPage', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
jiraConnectOauthEnabled | publicKeyStorageEnabled | shouldRenderDotCom | shouldRenderMultiversion
${false} | ${true} | ${true} | ${false}
diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
index 4956af76ead..d262f4b2735 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
@@ -26,10 +26,6 @@ describe('SubscriptionsPage', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe.each`
scenario | subscriptionsLoading | hasSubscriptions | expectSubscriptionsList | expectEmptyState
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 40e627262db..6766456d09c 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -2,7 +2,7 @@
exports[`JiraImportForm table body shows correct information in each cell 1`] = `
<table
- aria-busy="false"
+ aria-busy=""
aria-colcount="3"
class="table b-table gl-table b-table-fixed"
role="table"
@@ -92,7 +92,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<button
aria-expanded="false"
- aria-haspopup="true"
+ aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
type="button"
>
@@ -217,7 +217,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<button
aria-expanded="false"
- aria-haspopup="true"
+ aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
type="button"
>
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 022a0f81aaa..dc1b75f5d9e 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -67,11 +67,6 @@ describe('JiraImportApp', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when Jira integration is not configured', () => {
beforeEach(() => {
wrapper = mountComponent({ isJiraConfigured: false });
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 d43a9f8a145..c7db9f429de 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -106,7 +106,6 @@ describe('JiraImportForm', () => {
axiosMock.restore();
mutateSpy.mockRestore();
querySpy.mockRestore();
- wrapper.destroy();
});
describe('select dropdown project selection', () => {
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 42356763492..c0d415a2130 100644
--- a/spec/frontend/jira_import/components/jira_import_progress_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js
@@ -25,11 +25,6 @@ describe('JiraImportProgress', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('empty state', () => {
beforeEach(() => {
wrapper = mountComponent();
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 0085a2b5572..5331467d669 100644
--- a/spec/frontend/jira_import/components/jira_import_setup_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js
@@ -17,11 +17,6 @@ describe('JiraImportSetup', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('contains illustration', () => {
expect(getGlEmptyStateProp('svgPath')).toBe(illustration);
});
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 14613775791..5ecddc7efd6 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
@@ -27,10 +27,6 @@ describe('Jobs filtered search', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays filtered search', () => {
createComponent();
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
index fbe5f6a2e11..6755b854f01 100644
--- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -45,10 +45,6 @@ describe('Job Status Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/jobs/components/job/artifacts_block_spec.js b/spec/frontend/jobs/components/job/artifacts_block_spec.js
index c75deb64d84..ea5d727bd08 100644
--- a/spec/frontend/jobs/components/job/artifacts_block_spec.js
+++ b/spec/frontend/jobs/components/job/artifacts_block_spec.js
@@ -55,11 +55,6 @@ describe('Artifacts block', () => {
locked: true,
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with expired artifacts that are not locked', () => {
beforeEach(() => {
wrapper = createWrapper({
diff --git a/spec/frontend/jobs/components/job/commit_block_spec.js b/spec/frontend/jobs/components/job/commit_block_spec.js
index 4fcc754c82c..1c28b5079d7 100644
--- a/spec/frontend/jobs/components/job/commit_block_spec.js
+++ b/spec/frontend/jobs/components/job/commit_block_spec.js
@@ -32,10 +32,6 @@ describe('Commit block', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without merge request', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/jobs/components/job/environments_block_spec.js b/spec/frontend/jobs/components/job/environments_block_spec.js
index 134533e2af8..ab36f79ea5e 100644
--- a/spec/frontend/jobs/components/job/environments_block_spec.js
+++ b/spec/frontend/jobs/components/job/environments_block_spec.js
@@ -51,11 +51,6 @@ describe('Environments block', () => {
const findEnvironmentLink = () => wrapper.find('[data-testid="job-environment-link"]');
const findClusterLink = () => wrapper.find('[data-testid="job-cluster-link"]');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with last deployment', () => {
it('renders info for most recent deployment', () => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/erased_block_spec.js b/spec/frontend/jobs/components/job/erased_block_spec.js
index c6aba01fa53..aeab676fc7e 100644
--- a/spec/frontend/jobs/components/job/erased_block_spec.js
+++ b/spec/frontend/jobs/components/job/erased_block_spec.js
@@ -18,10 +18,6 @@ describe('Erased block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with job erased by user', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index cefedcd82fb..394fc8ad43c 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -83,8 +83,9 @@ describe('Job App', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
});
describe('while loading', () => {
diff --git a/spec/frontend/jobs/components/job/job_container_item_spec.js b/spec/frontend/jobs/components/job/job_container_item_spec.js
index 05c38dd74b7..8121aa1172f 100644
--- a/spec/frontend/jobs/components/job/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job/job_container_item_spec.js
@@ -24,11 +24,6 @@ describe('JobContainerItem', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when a job is not active and not retried', () => {
beforeEach(() => {
createComponent(job);
diff --git a/spec/frontend/jobs/components/job/job_log_controllers_spec.js b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
index 5e9a73b4387..9917c63b2d0 100644
--- a/spec/frontend/jobs/components/job/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
@@ -285,6 +285,18 @@ describe('Job log controllers', () => {
expect(findScrollFailure().props('disabled')).toBe(false);
});
});
+
+ describe('on error', () => {
+ beforeEach(() => {
+ jest.spyOn(commonUtils, 'backOff').mockRejectedValueOnce();
+
+ createWrapper({}, { jobLogJumpToFailures: true });
+ });
+
+ it('stays disabled', () => {
+ expect(findScrollFailure().props('disabled')).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
index d60043f33f7..712269a1e83 100644
--- a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
+++ b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
@@ -64,13 +64,11 @@ describe('Job Retry Forward Deployment Modal', () => {
beforeEach(createWrapper);
it('should correctly configure the primary action', () => {
- expect(findModal().props('actionPrimary').attributes).toMatchObject([
- {
- 'data-method': 'post',
- href: job.retry_path,
- variant: 'danger',
- },
- ]);
+ expect(findModal().props('actionPrimary').attributes).toMatchObject({
+ 'data-method': 'post',
+ href: job.retry_path,
+ variant: 'danger',
+ });
});
});
});
diff --git a/spec/frontend/jobs/components/job/jobs_container_spec.js b/spec/frontend/jobs/components/job/jobs_container_spec.js
index 2fde4d3020b..05660880751 100644
--- a/spec/frontend/jobs/components/job/jobs_container_spec.js
+++ b/spec/frontend/jobs/components/job/jobs_container_spec.js
@@ -68,10 +68,6 @@ describe('Jobs List block', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a list of jobs', () => {
createComponent({
jobs: [job, retried, active],
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index a5b3b0e3b47..98b9ca78a45 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -2,24 +2,28 @@ import { GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
+import { createAlert } from '~/alert';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants';
+import { JOB_GRAPHQL_ERRORS } from '~/jobs/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import waitForPromises from 'helpers/wait_for_promises';
import { redirectTo } from '~/lib/utils/url_utility';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
-import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
+import playJobMutation from '~/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql';
import {
mockFullPath,
mockId,
mockJobResponse,
mockJobWithVariablesResponse,
- mockJobMutationData,
+ mockJobPlayMutationData,
+ mockJobRetryMutationData,
} from './mock_data';
const localVue = createLocalVue();
+jest.mock('~/alert');
localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
@@ -39,9 +43,9 @@ describe('Manual Variables Form', () => {
const createComponent = ({ options = {}, props = {} } = {}) => {
wrapper = mountExtended(ManualVariablesForm, {
propsData: {
- ...props,
jobId: mockId,
- isRetryable: true,
+ isRetryable: false,
+ ...props,
},
provide: {
...defaultProvide,
@@ -71,7 +75,7 @@ describe('Manual Variables Form', () => {
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findCancelBtn = () => wrapper.findByTestId('cancel-btn');
- const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn');
+ const findRunBtn = () => wrapper.findByTestId('run-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');
@@ -97,7 +101,7 @@ describe('Manual Variables Form', () => {
});
afterEach(() => {
- wrapper.destroy();
+ createAlert.mockClear();
});
describe('when page renders', () => {
@@ -112,10 +116,30 @@ describe('Manual Variables Form', () => {
'/help/ci/variables/index#add-a-cicd-variable-to-a-project',
);
});
+ });
- it('renders buttons', () => {
- expect(findCancelBtn().exists()).toBe(true);
- expect(findRerunBtn().exists()).toBe(true);
+ describe('when query is unsuccessful', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockRejectedValue({});
+ await createComponentWithApollo();
+ });
+
+ it('shows an alert with error', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText,
+ });
+ });
+ });
+
+ describe('when job has not been retried', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse);
+ await createComponentWithApollo();
+ });
+
+ it('does not render the cancel button', () => {
+ expect(findCancelBtn().exists()).toBe(false);
+ expect(findRunBtn().exists()).toBe(true);
});
});
@@ -135,10 +159,10 @@ describe('Manual Variables Form', () => {
});
});
- describe('when mutation fires', () => {
+ describe('when play mutation fires', () => {
beforeEach(async () => {
await createComponentWithApollo();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData);
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobPlayMutationData);
});
it('passes variables in correct format', async () => {
@@ -146,11 +170,11 @@ describe('Manual Variables Form', () => {
await findCiVariableValue().setValue('new value');
- await findRerunBtn().vm.$emit('click');
+ await findRunBtn().vm.$emit('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: retryJobMutation,
+ mutation: playJobMutation,
variables: {
id: convertToGraphQLId(TYPENAME_CI_BUILD, mockId),
variables: [
@@ -163,13 +187,63 @@ describe('Manual Variables Form', () => {
});
});
- // redirect to job after initial trigger assertion will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+ it('redirects to job properly after job is run', async () => {
+ findRunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(mockJobPlayMutationData.data.jobPlay.job.webPath);
+ });
+ });
+
+ describe('when play mutation is unsuccessful', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
+ await createComponentWithApollo();
+ });
+
+ it('shows an alert with error', async () => {
+ findRunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText,
+ });
+ });
+ });
+
+ describe('when job is retryable', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({ props: { isRetryable: true } });
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobRetryMutationData);
+ });
+
+ it('renders cancel button', () => {
+ expect(findCancelBtn().exists()).toBe(true);
+ });
+
it('redirects to job properly after rerun', async () => {
- findRerunBtn().vm.$emit('click');
+ findRunBtn().vm.$emit('click');
await waitForPromises();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(redirectTo).toHaveBeenCalledWith(mockJobMutationData.data.jobRetry.job.webPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockJobRetryMutationData.data.jobRetry.job.webPath);
+ });
+ });
+
+ describe('when retry mutation is unsuccessful', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
+ await createComponentWithApollo({ props: { isRetryable: true } });
+ });
+
+ it('shows an alert with error', async () => {
+ findRunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText,
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
index 8a838acca7a..fb3a361c9c9 100644
--- a/spec/frontend/jobs/components/job/mock_data.js
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -50,7 +50,32 @@ export const mockJobWithVariablesResponse = {
},
};
-export const mockJobMutationData = {
+export const mockJobPlayMutationData = {
+ data: {
+ jobPlay: {
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualVariables: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::JobVariable/151',
+ key: 'new key',
+ value: 'new value',
+ __typename: 'CiManualVariable',
+ },
+ ],
+ __typename: 'CiManualVariableConnection',
+ },
+ webPath: '/Commit451/lab-coat/-/jobs/401',
+ __typename: 'CiJob',
+ },
+ errors: [],
+ __typename: 'JobPlayPayload',
+ },
+ },
+};
+
+export const mockJobRetryMutationData = {
data: {
jobRetry: {
job: {
diff --git a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
index 5c9c011b4ab..dd5a9e3491d 100644
--- a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
@@ -19,11 +19,6 @@ describe('Sidebar detail row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with title/value and without helpUrl', () => {
beforeEach(() => {
createComponent({ title, value });
diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index aa9ca932023..cefa4582c15 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -48,10 +48,6 @@ describe('Sidebar details block', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without terminal path', () => {
it('does not render terminal link', async () => {
createWrapper();
diff --git a/spec/frontend/jobs/components/job/stages_dropdown_spec.js b/spec/frontend/jobs/components/job/stages_dropdown_spec.js
index 61dec585e82..f782d5600e6 100644
--- a/spec/frontend/jobs/components/job/stages_dropdown_spec.js
+++ b/spec/frontend/jobs/components/job/stages_dropdown_spec.js
@@ -37,10 +37,6 @@ describe('Stages Dropdown', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without a merge request pipeline', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/trigger_block_spec.js b/spec/frontend/jobs/components/job/trigger_block_spec.js
index a1de8fd143f..8bb2c1f3ad8 100644
--- a/spec/frontend/jobs/components/job/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/job/trigger_block_spec.js
@@ -20,10 +20,6 @@ describe('Trigger block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with short token and no variables', () => {
it('renders short token', () => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
index fb7d389c4d6..1072cdd6781 100644
--- a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
@@ -18,10 +18,6 @@ describe('Unmet Prerequisites Block Job component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders an alert with the correct message', () => {
const container = wrapper.findComponent(GlAlert);
const alertMessage =
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 646935568b1..5adedea28a5 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -19,10 +19,6 @@ describe('Job Log Collapsible Section', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with closed section', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jobs/components/log/duration_badge_spec.js b/spec/frontend/jobs/components/log/duration_badge_spec.js
index 84dae386bdb..644d05366a0 100644
--- a/spec/frontend/jobs/components/log/duration_badge_spec.js
+++ b/spec/frontend/jobs/components/log/duration_badge_spec.js
@@ -20,10 +20,6 @@ describe('Job Log Duration Badge', () => {
createComponent(data);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders provided duration', () => {
expect(wrapper.text()).toBe(data.duration);
});
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index ec8e79bba13..16fe753e08a 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -29,10 +29,6 @@ describe('Job Log Header Line', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('line', () => {
beforeEach(() => {
createComponent(data);
diff --git a/spec/frontend/jobs/components/log/line_number_spec.js b/spec/frontend/jobs/components/log/line_number_spec.js
index 96aa31baab9..4130c124a30 100644
--- a/spec/frontend/jobs/components/log/line_number_spec.js
+++ b/spec/frontend/jobs/components/log/line_number_spec.js
@@ -21,10 +21,6 @@ describe('Job Log Line Number', () => {
createComponent(data);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders incremented lineNunber by 1', () => {
expect(wrapper.text()).toBe('1');
});
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index c933ed5c3e1..265f72ff344 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import Log from '~/jobs/components/log/log.vue';
+import LogLineHeader from '~/jobs/components/log/line_header.vue';
import { logLinesParser } from '~/jobs/store/utils';
import { jobLog } from './mock_data';
@@ -10,6 +11,7 @@ describe('Job Log', () => {
let actions;
let state;
let store;
+ let toggleCollapsibleLineMock;
Vue.use(Vuex);
@@ -20,8 +22,9 @@ describe('Job Log', () => {
};
beforeEach(() => {
+ toggleCollapsibleLineMock = jest.fn();
actions = {
- toggleCollapsibleLine: () => {},
+ toggleCollapsibleLine: toggleCollapsibleLineMock,
};
state = {
@@ -37,11 +40,7 @@ describe('Job Log', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findCollapsibleLine = () => wrapper.find('.collapsible-line');
+ const findCollapsibleLine = () => wrapper.findComponent(LogLineHeader);
describe('line numbers', () => {
it('renders a line number for each open line', () => {
@@ -68,11 +67,9 @@ describe('Job Log', () => {
describe('on click header section', () => {
it('calls toggleCollapsibleLine', () => {
- jest.spyOn(wrapper.vm, 'toggleCollapsibleLine');
-
findCollapsibleLine().trigger('click');
- expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled();
+ expect(toggleCollapsibleLineMock).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
index 763a4b0eaa2..d015edb0e91 100644
--- a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
@@ -22,10 +22,6 @@ describe('Duration Cell', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not display duration or finished time when no properties are present', () => {
createComponent();
diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
index ddc196129a7..73e37eed5f1 100644
--- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
@@ -39,10 +39,6 @@ describe('Job Cell', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Job Id', () => {
it('displays the job id and links to the job', () => {
createComponent();
diff --git a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
index 1f5e0a7aa21..3d424b20964 100644
--- a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
@@ -42,10 +42,6 @@ describe('Pipeline Cell', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Pipeline Id', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
index 88c97285b85..e3b1ca1cce3 100644
--- a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
+++ b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
@@ -84,4 +84,23 @@ describe('jobs/components/table/graphql/cache_config', () => {
expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length);
});
});
+
+ describe('when incoming data has no nodes', () => {
+ it('should return existing cache', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ { __typename: 'CiJobConnection', count: 500 },
+ {
+ args: { statuses: 'SUCCESS' },
+ },
+ );
+
+ const expectedResponse = {
+ ...CIJobConnectionExistingCache,
+ statuses: 'SUCCESS',
+ };
+
+ expect(res).toEqual(expectedResponse);
+ });
+ });
});
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 109cef6f817..6247cfcc640 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -12,8 +12,9 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
+import getJobsCountQuery from '~/jobs/components/table/graphql/queries/get_jobs_count.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';
@@ -23,12 +24,13 @@ import {
mockJobsResponsePaginated,
mockJobsResponseEmpty,
mockFailedSearchToken,
+ mockJobsCountResponse,
} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Job table app', () => {
let wrapper;
@@ -37,6 +39,8 @@ describe('Job table app', () => {
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty);
+ const countSuccessHandler = jest.fn().mockResolvedValue(mockJobsCountResponse);
+
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(JobsTable);
@@ -48,14 +52,18 @@ describe('Job table app', () => {
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
- const createMockApolloProvider = (handler) => {
- const requestHandlers = [[getJobsQuery, handler]];
+ const createMockApolloProvider = (handler, countHandler) => {
+ const requestHandlers = [
+ [getJobsQuery, handler],
+ [getJobsCountQuery, countHandler],
+ ];
return createMockApollo(requestHandlers);
};
const createComponent = ({
handler = successHandler,
+ countHandler = countSuccessHandler,
mountFn = shallowMount,
data = {},
} = {}) => {
@@ -68,14 +76,10 @@ describe('Job table app', () => {
provide: {
fullPath: projectPath,
},
- apolloProvider: createMockApolloProvider(handler),
+ apolloProvider: createMockApolloProvider(handler, countHandler),
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('should display skeleton loader when loading', () => {
createComponent();
@@ -148,12 +152,39 @@ describe('Job table app', () => {
});
describe('error state', () => {
- it('should show an alert if there is an error fetching the data', async () => {
+ it('should show an alert if there is an error fetching the jobs data', async () => {
createComponent({ handler: failedHandler });
await waitForPromises();
- expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('There was an error fetching the jobs for your project.');
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('should show an alert if there is an error fetching the jobs count data', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(
+ 'There was an error fetching the number of jobs for your project.',
+ );
+ });
+
+ it('jobs table should still load if count query fails', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('jobs count should be zero if count query fails', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findTabs().props('allJobsCount')).toBe(0);
});
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
index 3c4f2d624fe..06b13aa4372 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -30,10 +30,6 @@ describe('Jobs Table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the jobs table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
index 23632001060..70bcac82a3f 100644
--- a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
@@ -42,10 +42,6 @@ describe('Jobs Table Tabs', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays All tab with count', () => {
expect(trimText(findAllTab().text())).toBe(`All ${defaultProps.allJobsCount}`);
});
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
index 1d3845b19bb..098a63719fe 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -16,11 +16,6 @@ describe('DelayedJobMixin', () => {
template: '<div>{{remainingTime}}</div>',
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('if job is empty object', () => {
beforeEach(() => {
wrapper = shallowMount(dummyComponent, {
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 9abd610c26d..483b4ca711f 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1,3 +1,4 @@
+import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json';
import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json';
import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json';
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
@@ -13,6 +14,7 @@ export const mockJobsResponsePaginated = mockJobsPaginated;
export const mockJobsResponseEmpty = mockJobsEmpty;
export const mockJobsNodes = mockJobs.data.project.jobs.nodes;
export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes;
+export const mockJobsCountResponse = mockJobsCount;
export const stages = [
{
diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js
index 24a803d3f16..7654d218209 100644
--- a/spec/frontend/labels/components/delete_label_modal_spec.js
+++ b/spec/frontend/labels/components/delete_label_modal_spec.js
@@ -30,10 +30,6 @@ describe('~/labels/components/delete_label_modal', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js
index 97913c20229..5983c16a9d1 100644
--- a/spec/frontend/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/labels/components/promote_label_modal_spec.js
@@ -41,7 +41,6 @@ describe('Promote label modal', () => {
afterEach(() => {
axiosMock.reset();
- wrapper.destroy();
});
describe('Modal title and description', () => {
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
index 7f6fb138d89..036ff55fef7 100644
--- a/spec/frontend/language_switcher/components/app_spec.js
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -24,10 +24,6 @@ describe('<LanguageSwitcher />', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const getPreferredLanguage = () => wrapper.find('.gl-new-dropdown-button-text').text();
const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
const findFooter = () => wrapper.findByTestId('footer');
diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
index 5ac7a7985a8..b8847f0fca3 100644
--- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -6,13 +6,8 @@ import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
jest.mock('~/lib/utils/is_navigating_away');
describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
- const originalGon = window.gon;
let subscription;
- beforeEach(() => {
- window.gon = originalGon;
- });
-
afterEach(() => {
if (subscription) {
subscription.unsubscribe();
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index f767a673553..fdc8789c1a8 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -49,8 +49,6 @@ const forbiddenDataAttrs = defaultConfig.FORBID_ATTR;
const acceptedDataAttrs = ['data-random', 'data-custom'];
describe('~/lib/dompurify', () => {
- let originalGon;
-
it('uses local configuration when given', () => {
// As dompurify uses a "Persistent Configuration", it might
// ignore config, this check verifies we respect
@@ -104,15 +102,10 @@ describe('~/lib/dompurify', () => {
${'root'} | ${rootGon}
${'absolute'} | ${absoluteGon}
`('when gon contains $type icon urls', ({ type, gon }) => {
- beforeAll(() => {
- originalGon = window.gon;
+ beforeEach(() => {
window.gon = gon;
});
- afterAll(() => {
- window.gon = originalGon;
- });
-
it('allows no href attrs', () => {
const htmlHref = `<svg><use></use></svg>`;
expect(sanitize(htmlHref)).toBe(htmlHref);
@@ -137,14 +130,9 @@ describe('~/lib/dompurify', () => {
describe('when gon does not contain icon urls', () => {
beforeAll(() => {
- originalGon = window.gon;
window.gon = {};
});
- afterAll(() => {
- window.gon = originalGon;
- });
-
it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', (url) => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js
index 4471b781446..3d063ff9b46 100644
--- a/spec/frontend/lib/utils/axios_startup_calls_spec.js
+++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js
@@ -113,17 +113,10 @@ describe('setupAxiosStartupCalls', () => {
});
describe('startup call', () => {
- let oldGon;
-
beforeEach(() => {
- oldGon = window.gon;
window.gon = { gitlab_url: 'https://example.org/gitlab' };
});
- afterEach(() => {
- window.gon = oldGon;
- });
-
it('removes GitLab Base URL from startup call', async () => {
window.gl.startup_calls = {
'/startup': {
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 7b068f7d248..b4ec00ab766 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -534,18 +534,10 @@ describe('common_utils', () => {
});
describe('spriteIcon', () => {
- let beforeGon;
-
beforeEach(() => {
- window.gon = window.gon || {};
- beforeGon = { ...window.gon };
window.gon.sprite_icons = 'icons.svg';
});
- afterEach(() => {
- window.gon = beforeGon;
- });
-
it('should return the svg for a linked icon', () => {
expect(commonUtils.spriteIcon('test')).toEqual(
'<svg ><use xlink:href="icons.svg#test" /></svg>',
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
index 142c76f7bc0..9b790e739fb 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
@@ -44,7 +44,6 @@ describe('confirmAction', () => {
resetHTMLFixture();
Vue.prototype.$mount.mockRestore();
modalWrapper?.destroy();
- modalWrapper = null;
modal?.destroy();
modal = null;
});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index 313e028d861..c135180c9df 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -28,10 +28,6 @@ describe('Confirm Modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlModal = () => wrapper.findComponent(GlModal);
describe('Modal events', () => {
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
index 1ef7047d959..c13d55f978e 100644
--- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -3,16 +3,6 @@ import { s__ } from '~/locale';
import '~/commons/bootstrap';
describe('TimeAgo utils', () => {
- let oldGon;
-
- afterEach(() => {
- window.gon = oldGon;
- });
-
- beforeEach(() => {
- oldGon = window.gon;
- });
-
describe('getTimeago', () => {
describe('with User Setting timeDisplayRelative: true', () => {
beforeEach(() => {
diff --git a/spec/frontend/lib/utils/error_message_spec.js b/spec/frontend/lib/utils/error_message_spec.js
new file mode 100644
index 00000000000..17b5168c32f
--- /dev/null
+++ b/spec/frontend/lib/utils/error_message_spec.js
@@ -0,0 +1,65 @@
+import { parseErrorMessage, USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
+
+const defaultErrorMessage = 'Something caused this error';
+const userFacingErrorMessage = 'User facing error message';
+const nonUserFacingErrorMessage = 'NonUser facing error message';
+const genericErrorMessage = 'Some error message';
+
+describe('error message', () => {
+ describe('when given an errormessage object', () => {
+ const errorMessageObject = {
+ options: {
+ cause: defaultErrorMessage,
+ },
+ filename: 'error.js',
+ linenumber: 7,
+ };
+
+ it('returns the correct values for userfacing errors', () => {
+ const userFacingObject = errorMessageObject;
+ userFacingObject.message = `${USER_FACING_ERROR_MESSAGE_PREFIX} ${userFacingErrorMessage}`;
+
+ expect(parseErrorMessage(userFacingObject)).toEqual({
+ message: userFacingErrorMessage,
+ userFacing: true,
+ });
+ });
+
+ it('returns the correct values for non userfacing errors', () => {
+ const nonUserFacingObject = errorMessageObject;
+ nonUserFacingObject.message = nonUserFacingErrorMessage;
+
+ expect(parseErrorMessage(nonUserFacingObject)).toEqual({
+ message: nonUserFacingErrorMessage,
+ userFacing: false,
+ });
+ });
+ });
+
+ describe('when given an errormessage string', () => {
+ it('returns the correct values for userfacing errors', () => {
+ expect(
+ parseErrorMessage(`${USER_FACING_ERROR_MESSAGE_PREFIX} ${genericErrorMessage}`),
+ ).toEqual({
+ message: genericErrorMessage,
+ userFacing: true,
+ });
+ });
+
+ it('returns the correct values for non userfacing errors', () => {
+ expect(parseErrorMessage(genericErrorMessage)).toEqual({
+ message: genericErrorMessage,
+ userFacing: false,
+ });
+ });
+ });
+
+ describe('when given nothing', () => {
+ it('returns an empty error message', () => {
+ expect(parseErrorMessage()).toEqual({
+ message: '',
+ userFacing: false,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index f63af2fe0a4..509ddc7ce86 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,5 +1,9 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
+import fileUpload, {
+ getFilename,
+ validateImageName,
+ validateFileFromAllowList,
+} from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
@@ -89,3 +93,19 @@ describe('file name validator', () => {
expect(validateImageName(file)).toBe('image.png');
});
});
+
+describe('validateFileFromAllowList', () => {
+ it('returns true if the file type is in the allowed list', () => {
+ const allowList = ['.foo', '.bar'];
+ const fileName = 'file.foo';
+
+ expect(validateFileFromAllowList(fileName, allowList)).toBe(true);
+ });
+
+ it('returns false if the file type is in the allowed list', () => {
+ const allowList = ['.foo', '.bar'];
+ const fileName = 'file.baz';
+
+ expect(validateFileFromAllowList(fileName, allowList)).toBe(false);
+ });
+});
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index dc4aa0ea5ed..d2591cd2328 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -3,6 +3,7 @@ import {
bytesToKiB,
bytesToMiB,
bytesToGiB,
+ numberToHumanSizeSplit,
numberToHumanSize,
numberToMetricPrefix,
sum,
@@ -13,6 +14,12 @@ import {
isNumeric,
isPositiveInteger,
} from '~/lib/utils/number_utils';
+import {
+ BYTES_FORMAT_BYTES,
+ BYTES_FORMAT_KIB,
+ BYTES_FORMAT_MIB,
+ BYTES_FORMAT_GIB,
+} from '~/lib/utils/constants';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -78,6 +85,28 @@ describe('Number Utils', () => {
});
});
+ describe('numberToHumanSizeSplit', () => {
+ it('should return bytes', () => {
+ expect(numberToHumanSizeSplit(654)).toEqual(['654', BYTES_FORMAT_BYTES]);
+ expect(numberToHumanSizeSplit(-654)).toEqual(['-654', BYTES_FORMAT_BYTES]);
+ });
+
+ it('should return KiB', () => {
+ expect(numberToHumanSizeSplit(1079)).toEqual(['1.05', BYTES_FORMAT_KIB]);
+ expect(numberToHumanSizeSplit(-1079)).toEqual(['-1.05', BYTES_FORMAT_KIB]);
+ });
+
+ it('should return MiB', () => {
+ expect(numberToHumanSizeSplit(10485764)).toEqual(['10.00', BYTES_FORMAT_MIB]);
+ expect(numberToHumanSizeSplit(-10485764)).toEqual(['-10.00', BYTES_FORMAT_MIB]);
+ });
+
+ it('should return GiB', () => {
+ expect(numberToHumanSizeSplit(10737418240)).toEqual(['10.00', BYTES_FORMAT_GIB]);
+ expect(numberToHumanSizeSplit(-10737418240)).toEqual(['-10.00', BYTES_FORMAT_GIB]);
+ });
+ });
+
describe('numberToHumanSize', () => {
it('should return bytes', () => {
expect(numberToHumanSize(654)).toEqual('654 bytes');
diff --git a/spec/frontend/lib/utils/ref_validator_spec.js b/spec/frontend/lib/utils/ref_validator_spec.js
new file mode 100644
index 00000000000..7185ebf0a24
--- /dev/null
+++ b/spec/frontend/lib/utils/ref_validator_spec.js
@@ -0,0 +1,79 @@
+import { validateTag, validationMessages } from '~/lib/utils/ref_validator';
+
+describe('~/lib/utils/ref_validator', () => {
+ describe('validateTag', () => {
+ describe.each([
+ ['foo'],
+ ['FOO'],
+ ['foo/a.lockx'],
+ ['foo.123'],
+ ['foo/123'],
+ ['foo/bar/123'],
+ ['foo.bar.123'],
+ ['foo-bar_baz'],
+ ['head'],
+ ['"foo"-'],
+ ['foo@bar'],
+ ['\ud83e\udd8a'],
+ ['ünicöde'],
+ ['\x80}'],
+ ])('tag with the name "%s"', (tagName) => {
+ it('is valid', () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(true);
+ expect(result.validationErrors).toEqual([]);
+ });
+ });
+
+ describe.each([
+ [' ', validationMessages.EmptyNameValidationMessage],
+
+ ['refs/heads/tagName', validationMessages.DisallowedPrefixesValidationMessage],
+ ['/foo', validationMessages.DisallowedPrefixesValidationMessage],
+ ['-tagName', validationMessages.DisallowedPrefixesValidationMessage],
+
+ ['HEAD', validationMessages.DisallowedNameValidationMessage],
+ ['@', validationMessages.DisallowedNameValidationMessage],
+
+ ['tag name with spaces', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag\\name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag^name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag..name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['..', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag?name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag*name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag[name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag@{name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag:name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag~name', validationMessages.DisallowedSubstringsValidationMessage],
+
+ ['/', validationMessages.DisallowedSequenceEmptyValidationMessage],
+ ['//', validationMessages.DisallowedSequenceEmptyValidationMessage],
+ ['foo//123', validationMessages.DisallowedSequenceEmptyValidationMessage],
+
+ ['.', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['/./', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['./.', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['.tagName', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['tag/.Name', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['foo/.123/bar', validationMessages.DisallowedSequencePrefixesValidationMessage],
+
+ ['foo.', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo/a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo/a.lock/b', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo.123.', validationMessages.DisallowedSequencePostfixesValidationMessage],
+
+ ['foo/', validationMessages.DisallowedPostfixesValidationMessage],
+
+ ['control-character\x7f', validationMessages.ControlCharactersValidationMessage],
+ ['control-character\x15', validationMessages.ControlCharactersValidationMessage],
+ ])('tag with name "%s"', (tagName, validationMessage) => {
+ it(`should be invalid with validation message "${validationMessage}"`, () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(false);
+ expect(result.validationErrors).toContain(validationMessage);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 7aab1013fc0..2180ea7e6c2 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -1,12 +1,16 @@
import $ from 'jquery';
+import AxiosMockAdapter from 'axios-mock-adapter';
import {
insertMarkdownText,
keypressNoteText,
compositionStartNoteText,
compositionEndNoteText,
updateTextForToolbarBtn,
+ resolveSelectedImage,
} from '~/lib/utils/text_markdown';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import '~/lib/utils/jquery_at_who';
+import axios from '~/lib/utils/axios_utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('init markdown', () => {
@@ -14,6 +18,7 @@ describe('init markdown', () => {
let textArea;
let indentButton;
let outdentButton;
+ let axiosMock;
beforeAll(() => {
setHTMLFixture(
@@ -34,6 +39,14 @@ describe('init markdown', () => {
document.execCommand = jest.fn(() => false);
});
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
afterAll(() => {
resetHTMLFixture();
});
@@ -707,6 +720,55 @@ describe('init markdown', () => {
});
});
+ describe('resolveSelectedImage', () => {
+ const markdownPreviewPath = '/markdown/preview';
+ const imageMarkdown = '![image](/uploads/image.png)';
+ const imageAbsoluteUrl = '/abs/uploads/image.png';
+
+ describe('when textarea cursor is positioned on an image', () => {
+ beforeEach(() => {
+ axiosMock.onPost(markdownPreviewPath, { text: imageMarkdown }).reply(HTTP_STATUS_OK, {
+ body: `
+ <p><a href="${imageAbsoluteUrl}"><img src="${imageAbsoluteUrl}"></a></p>
+ `,
+ });
+ });
+
+ it('returns the image absolute URL, markdown, and filename', async () => {
+ textArea.value = `image ${imageMarkdown}`;
+ textArea.setSelectionRange(8, 8);
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toEqual({
+ imageURL: imageAbsoluteUrl,
+ imageMarkdown,
+ filename: 'image.png',
+ });
+ });
+ });
+
+ describe('when textarea cursor is not positioned on an image', () => {
+ it.each`
+ markdown | selectionRange
+ ${`image ${imageMarkdown}`} | ${[4, 4]}
+ ${`!2 (issue)`} | ${[2, 2]}
+ `('returns null', async ({ markdown, selectionRange }) => {
+ textArea.value = markdown;
+ textArea.setSelectionRange(...selectionRange);
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null);
+ });
+ });
+
+ describe('when textarea cursor is positioned between images', () => {
+ it('returns null', async () => {
+ const position = imageMarkdown.length + 1;
+
+ textArea.value = `${imageMarkdown}\n\n${imageMarkdown}`;
+ textArea.setSelectionRange(position, position);
+
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null);
+ });
+ });
+ });
+
describe('Source Editor', () => {
let editor;
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index f2572ca0ad2..71a84d56791 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -398,4 +398,36 @@ describe('text_utility', () => {
expect(textUtils.base64DecodeUnicode('8J+YgA==')).toBe('😀');
});
});
+
+ describe('findInvalidBranchNameCharacters', () => {
+ const invalidChars = [' ', '~', '^', ':', '?', '*', '[', '..', '@{', '\\', '//'];
+ const badBranchName = 'branch-with all these ~ ^ : ? * [ ] \\ // .. @{ } //';
+ const goodBranch = 'branch-with-no-errrors';
+
+ it('returns an array of invalid characters in a branch name', () => {
+ const chars = textUtils.findInvalidBranchNameCharacters(badBranchName);
+ chars.forEach((char) => {
+ expect(invalidChars).toContain(char);
+ });
+ });
+
+ it('returns an empty array with no invalid characters', () => {
+ expect(textUtils.findInvalidBranchNameCharacters(goodBranch)).toEqual([]);
+ });
+ });
+
+ describe('humanizeBranchValidationErrors', () => {
+ it.each`
+ errors | message
+ ${[' ']} | ${"Can't contain spaces"}
+ ${['?', '//', ' ']} | ${"Can't contain spaces, ?, //"}
+ ${['\\', '[', '..']} | ${"Can't contain \\, [, .."}
+ `('returns an $message with $errors', ({ errors, message }) => {
+ expect(textUtils.humanizeBranchValidationErrors(errors)).toEqual(message);
+ });
+
+ it('returns an empty string with no invalid characters', () => {
+ expect(textUtils.humanizeBranchValidationErrors([])).toEqual('');
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 6afdab455a6..72556e6bbe2 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -45,10 +45,6 @@ describe('URL utility', () => {
});
describe('webIDEUrl', () => {
- afterEach(() => {
- gon.relative_url_root = '';
- });
-
it('escapes special characters', () => {
expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-#-foss/merge_requests/1')).toBe(
'/-/ide/project/gitlab-org/gitlab-%23-foss/merge_requests/1',
@@ -505,10 +501,6 @@ describe('URL utility', () => {
gon.gitlab_url = gitlabUrl;
});
- afterEach(() => {
- gon.gitlab_url = '';
- });
-
it.each`
url | urlType | external
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
index d25a692dfea..abd5095c1d2 100644
--- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js
+++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
@@ -96,10 +96,6 @@ describe('~/lib/utils/vuex_module_mappers', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('from module defined by prop', () => {
it('maps state', () => {
expect(getMappedState()).toEqual({
diff --git a/spec/frontend/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js
index e0d0e117ea4..a7e245e2b78 100644
--- a/spec/frontend/locale/sprintf_spec.js
+++ b/spec/frontend/locale/sprintf_spec.js
@@ -84,5 +84,16 @@ describe('locale', () => {
expect(output).toBe('contains duplicated 15%');
});
});
+
+ describe('ignores special replacements in the input', () => {
+ it.each(['$$', '$&', '$`', `$'`])('replacement "%s" is ignored', (replacement) => {
+ const input = 'My odd %{replacement} is preserved';
+
+ const parameters = { replacement };
+
+ const output = sprintf(input, parameters, false);
+ expect(output).toBe(`My odd ${replacement} is preserved`);
+ });
+ });
});
});
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index b94964dc482..c2e0e44f97f 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -20,10 +20,6 @@ describe('AccessRequestActionButtons', () => {
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
const findApproveButton = () => wrapper.findComponent(ApproveAccessRequestButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders remove member button', () => {
createComponent();
diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index 15bb03480e1..7a4cd844425 100644
--- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -38,7 +38,7 @@ describe('ApproveAccessRequestButton', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -50,10 +50,6 @@ describe('ApproveAccessRequestButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a tooltip', () => {
const button = findButton();
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index 68009708c99..a852443844b 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -19,10 +19,6 @@ describe('InviteActionButtons', () => {
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
const findResendInviteButton = () => wrapper.findComponent(ResendInviteButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user has `canRemove` permissions', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index b511cebdf28..1d83a2e0e71 100644
--- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -37,7 +37,7 @@ describe('RemoveGroupLinkButton', () => {
groupLink: group,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -48,11 +48,6 @@ describe('RemoveGroupLinkButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays a tooltip', () => {
const button = findButton();
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index cca340169b7..3879279b559 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -47,7 +47,7 @@ describe('RemoveMemberButton', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -58,10 +58,6 @@ describe('RemoveMemberButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets attributes on button', () => {
expect(wrapper.attributes()).toMatchObject({
'aria-label': 'Remove member',
diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 51cfd47ddf4..a6b5978b566 100644
--- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -38,7 +38,7 @@ describe('ResendInviteButton', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -50,10 +50,6 @@ describe('ResendInviteButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a tooltip', () => {
expect(getBinding(findButton().element, 'gl-tooltip')).not.toBeUndefined();
expect(findButton().attributes('title')).toBe('Resend invite');
diff --git a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
index 90f5b217007..679ad7897ed 100644
--- a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
+++ b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
@@ -18,7 +18,7 @@ describe('LeaveGroupDropdownItem', () => {
...propsData,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
slots: {
default: text,
@@ -32,10 +32,6 @@ describe('LeaveGroupDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a slot with red text', () => {
expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
});
diff --git a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
index e1c498249d7..125f1f8fff3 100644
--- a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
+++ b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
@@ -58,10 +58,6 @@ describe('RemoveMemberDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a slot with red text', () => {
expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
});
diff --git a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
index 5a2de1cac80..448c04bcb69 100644
--- a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
+++ b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
@@ -24,17 +24,13 @@ describe('UserActionDropdown', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const findRemoveMemberDropdownItem = () => wrapper.findComponent(RemoveMemberDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user has `canRemove` permissions', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index d105a4d9fde..b2147163233 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -49,7 +49,6 @@ describe('MembersApp', () => {
});
afterEach(() => {
- wrapper.destroy();
store = null;
});
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 13c50de9835..8e4263f88fe 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -25,10 +25,6 @@ describe('MemberList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders link to group', () => {
const link = wrapper.findComponent(GlAvatarLink);
diff --git a/spec/frontend/members/components/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js
index b197a46c0d1..84878fb9be2 100644
--- a/spec/frontend/members/components/avatars/invite_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js
@@ -24,10 +24,6 @@ describe('MemberList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders email as name', () => {
expect(getByText(invite.email).exists()).toBe(true);
});
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 9172876e76f..4808bcb9363 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -26,10 +26,6 @@ describe('UserAvatar', () => {
const findStatusEmoji = (emoji) => wrapper.find(`gl-emoji[data-name="${emoji}"]`);
- afterEach(() => {
- wrapper.destroy();
- });
-
it("renders link to user's profile", () => {
createComponent();
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index ef3c8bde3cf..526f839ece8 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -46,7 +46,7 @@ describe('SortDropdown', () => {
const findSortingComponent = () => wrapper.findComponent(GlSorting);
const findSortDirectionToggle = () =>
findSortingComponent().find('button[title^="Sort direction"]');
- const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
+ const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]');
const findDropdownItemByText = (text) =>
wrapper
.findAllComponents(GlSortingItem)
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 77af5e7293e..9078bd87d62 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -100,10 +100,6 @@ describe('MembersTabs', () => {
setWindowLocation('https://localhost');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', async () => {
await createComponent();
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index ba587c6f0b3..cec5f192e59 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -60,10 +60,6 @@ describe('LeaveModal', () => {
const findForm = () => findModal().findComponent(GlForm);
const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets modal ID', async () => {
await createComponent();
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index af96396f09f..e4782ac7f2e 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -52,11 +52,6 @@ describe('RemoveGroupLinkModal', () => {
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when modal is open', () => {
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js
index 47a03b5083a..baef0b30b02 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -54,10 +54,6 @@ describe('RemoveMemberModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
state | memberModelType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
${'removing a group member'} | ${MEMBER_MODEL_TYPE_GROUP_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index fa31177564b..2c0493e7c59 100644
--- a/spec/frontend/members/components/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -20,10 +20,6 @@ describe('CreatedAt', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('created at text', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index 9b8f053348b..15812ee6572 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -58,10 +58,6 @@ describe('ExpirationDatepicker', () => {
const findInput = () => wrapper.find('input');
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('datepicker input', () => {
it('sets `member.expiresAt` as initial date', async () => {
createComponent({ member: { ...member, expiresAt: '2020-03-17T00:00:00Z' } });
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index 95db30a3683..3a04d1dcb0a 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -23,10 +23,6 @@ describe('MemberActions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
memberType | member | expectedComponent | expectedComponentName
${MEMBER_TYPES.user} | ${memberMock} | ${UserActionDropdown} | ${'UserActionDropdown'}
diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index dc5c97f41df..369f8a06cfd 100644
--- a/spec/frontend/members/components/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -18,10 +18,6 @@ describe('MemberList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
memberType | member | expectedComponent | expectedComponentName
${MEMBER_TYPES.user} | ${memberMock} | ${UserAvatar} | ${'UserAvatar'}
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index fbfd0ca7ae7..bbfbb19fd92 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -23,17 +23,13 @@ describe('MemberSource', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('direct member', () => {
describe('when created by is available', () => {
it('displays "Direct member by <user name>"', () => {
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index ac5d83d028d..1c6f1b086cf 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -97,11 +97,6 @@ describe('MembersTableCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
member | expectedMemberType
${memberMock} | ${MEMBER_TYPES.user}
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index b8e0d73d8f6..e3c89bfed53 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -96,10 +96,6 @@ describe('MembersTable', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('fields', () => {
const memberCanUpdate = {
...directMember,
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index a11f67be8f5..d6e63b1930f 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -67,21 +67,13 @@ describe('RoleDropdown', () => {
.findAllComponents(GlDropdownItem)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked'));
- const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
+ const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]');
const findDropdown = () => wrapper.findComponent(GlDropdown);
- let originalGon;
-
beforeEach(() => {
- originalGon = window.gon;
gon.features = { showOverageOnRolePromotion: true };
});
- afterEach(() => {
- window.gon = originalGon;
- wrapper.destroy();
- });
-
describe('when dropdown is open', () => {
beforeEach(() => {
guestOverageConfirmAction.mockReturnValue(true);
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index 5c813eb2a67..b1730cf3746 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -31,9 +31,6 @@ describe('initMembersApp', () => {
afterEach(() => {
el = null;
-
- wrapper.destroy();
- wrapper = null;
});
it('renders `MembersTabs`', () => {
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 9b5641ef7b3..ab913b30f3c 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
@@ -37,10 +37,6 @@ describe('Merge Conflict Resolver App', () => {
store.dispatch('setConflictsData', conflictsMock);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findLoadingSpinner = () => wrapper.findByTestId('loading-spinner');
const findConflictsCount = () => wrapper.findByTestId('conflicts-count');
const findFiles = () => wrapper.findAllByTestId('files');
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 19ef4b7db25..d2c4c8b796c 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -4,13 +4,13 @@ import Cookies from '~/lib/utils/cookies';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '~/merge_conflicts/constants';
import * as actions from '~/merge_conflicts/store/actions';
import * as types from '~/merge_conflicts/store/mutation_types';
import { restoreFileLinesState, markLine, decorateFiles } from '~/merge_conflicts/utils';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/merge_conflicts/utils');
jest.mock('~/lib/utils/cookies');
@@ -114,7 +114,7 @@ describe('merge conflicts actions', () => {
expect(window.location.assign).toHaveBeenCalledWith('hrefPath');
});
- it('on errors shows flash', async () => {
+ it('on errors shows an alert', async () => {
mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.submitResolvedConflicts,
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 579cee8c022..be16b5ebfd2 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -3,12 +3,12 @@ import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_CONFLICT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MergeRequest from '~/merge_request';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('MergeRequest', () => {
const test = {};
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 6d434d7e654..76d1fa3a332 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -354,8 +354,6 @@ describe('MergeRequestTabs', () => {
testContext.class.expandSidebar.forEach((el) => {
expect(el.classList.contains('gl-display-none!')).toBe(hides);
});
-
- window.gon = {};
});
describe('when switching tabs', () => {
diff --git a/spec/frontend/merge_requests/components/compare_app_spec.js b/spec/frontend/merge_requests/components/compare_app_spec.js
index 8f84341b653..ba129363ffd 100644
--- a/spec/frontend/merge_requests/components/compare_app_spec.js
+++ b/spec/frontend/merge_requests/components/compare_app_spec.js
@@ -30,10 +30,6 @@ function factory(provideData = {}) {
}
describe('Merge requests compare app component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows commit box when selected branch is empty', () => {
factory({
currentBranch: {
diff --git a/spec/frontend/merge_requests/components/compare_dropdown_spec.js b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
index ab5c315816c..ce03b80bdcb 100644
--- a/spec/frontend/merge_requests/components/compare_dropdown_spec.js
+++ b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
@@ -47,7 +47,6 @@ describe('Merge requests compare dropdown component', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
index 87235fa843a..f8730fd93a3 100644
--- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
@@ -6,10 +6,10 @@ import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal
import eventHub from '~/milestones/event_hub';
import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Delete milestone modal', () => {
let wrapper;
@@ -39,10 +39,6 @@ describe('Delete milestone modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('onSubmit', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index f8ddca1a2ad..748e01d4291 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -85,11 +85,6 @@ describe('Milestone combobox component', () => {
mock.onGet(`/api/v4/projects/${projectId}/search`).reply((config) => searchApiCallSpy(config));
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
//
// Finders
//
diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
index d7ad3d29d0a..e91e792afe8 100644
--- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
@@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Promote milestone modal', () => {
let wrapper;
@@ -33,10 +33,6 @@ describe('Promote milestone modal', () => {
wrapper = shallowMount(PromoteMilestoneModal);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Modal opener button', () => {
it('button gets disabled when the modal opens', () => {
expect(promoteButton().disabled).toBe(false);
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
index 7d7eee2bc2c..dc21db39259 100644
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`MlCandidate renders correctly 1`] = `
+exports[`MlCandidatesShow renders correctly 1`] = `
<div>
<div
class="gl-alert gl-alert-warning"
@@ -152,7 +152,24 @@ exports[`MlCandidate renders correctly 1`] = `
</td>
</tr>
- <!---->
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ Artifacts
+ </td>
+
+ <td>
+ <a
+ class="gl-link"
+ href="path_to_artifact"
+ >
+ Artifacts
+ </a>
+ </td>
+ </tr>
<tr
class="divider"
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
index 483e454d7d7..36455339041 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -1,8 +1,8 @@
import { GlAlert } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
+import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show';
-describe('MlCandidate', () => {
+describe('MlCandidatesShow', () => {
let wrapper;
const createWrapper = () => {
@@ -21,14 +21,14 @@ describe('MlCandidate', () => {
],
info: {
iid: 'candidate_iid',
- artifact_link: 'path_to_artifact',
+ path_to_artifact: 'path_to_artifact',
experiment_name: 'The Experiment',
experiment_path: 'path/to/experiment',
status: 'SUCCESS',
},
};
- return mountExtended(MlCandidate, { propsData: { candidate } });
+ return mountExtended(MlCandidatesShow, { propsData: { candidate } });
};
const findAlert = () => wrapper.findComponent(GlAlert);
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
index f307d2c5a58..97a5049ea88 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
@@ -1,87 +1,35 @@
-import { GlAlert, GlTable, GlLink } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import { GlAlert, GlTableLite, GlLink, GlEmptyState } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
+import MlExperimentsShow from '~/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlHelpers from '~/lib/utils/url_utility';
+import { MOCK_START_CURSOR, MOCK_PAGE_INFO, MOCK_CANDIDATES } from './mock_data';
-describe('MlExperiment', () => {
+describe('MlExperimentsShow', () => {
let wrapper;
- const startCursor = 'eyJpZCI6IjE2In0';
- const defaultPageInfo = {
- startCursor,
- endCursor: 'eyJpZCI6IjIifQ',
- hasNextPage: true,
- hasPreviousPage: true,
- };
-
const createWrapper = (
candidates = [],
metricNames = [],
paramNames = [],
- pageInfo = defaultPageInfo,
+ pageInfo = MOCK_PAGE_INFO,
) => {
- wrapper = mountExtended(MlExperiment, {
- provide: { candidates, metricNames, paramNames, pageInfo },
+ wrapper = mountExtended(MlExperimentsShow, {
+ propsData: { candidates, metricNames, paramNames, pageInfo },
});
};
- const candidates = [
- {
- rmse: 1,
- l1_ratio: 0.4,
- details: 'link_to_candidate1',
- artifact: 'link_to_artifact',
- name: 'aCandidate',
- created_at: '2023-01-05T14:07:02.975Z',
- user: { username: 'root', path: '/root' },
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate2',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate3',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate4',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate5',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- ];
-
- const createWrapperWithCandidates = (pageInfo = defaultPageInfo) => {
- createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
+ const createWrapperWithCandidates = (pageInfo = MOCK_PAGE_INFO) => {
+ createWrapper(MOCK_CANDIDATES, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
};
const findAlert = () => wrapper.findComponent(GlAlert);
const findPagination = () => wrapper.findComponent(Pagination);
- const findEmptyState = () => wrapper.findByText('No candidates to display');
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
- const findTable = () => wrapper.findComponent(GlTable);
+ const findTable = () => wrapper.findComponent(GlTableLite);
const findTableHeaders = () => findTable().findAll('th');
const findTableRows = () => findTable().findAll('tbody > tr');
const findNthTableRow = (idx) => findTableRows().at(idx);
@@ -98,8 +46,6 @@ describe('MlExperiment', () => {
describe('default inputs', () => {
beforeEach(async () => {
createWrapper();
-
- await nextTick();
});
it('shows empty state', () => {
@@ -110,8 +56,8 @@ describe('MlExperiment', () => {
expect(findPagination().exists()).toBe(false);
});
- it('there are no columns', () => {
- expect(findTable().findAll('th')).toHaveLength(0);
+ it('does not show table', () => {
+ expect(findTable().exists()).toBe(false);
});
it('initializes sorting correctly', () => {
@@ -227,34 +173,33 @@ describe('MlExperiment', () => {
it('Passes pagination to pagination component', () => {
createWrapperWithCandidates();
- expect(findPagination().props('startCursor')).toBe(startCursor);
+ expect(findPagination().props('startCursor')).toBe(MOCK_START_CURSOR);
});
});
describe('Candidate table', () => {
const firstCandidateIndex = 0;
const secondCandidateIndex = 1;
- const firstCandidate = candidates[firstCandidateIndex];
+ const firstCandidate = MOCK_CANDIDATES[firstCandidateIndex];
beforeEach(() => {
createWrapperWithCandidates();
});
it('renders all rows', () => {
- expect(findTableRows()).toHaveLength(candidates.length);
+ expect(findTableRows()).toHaveLength(MOCK_CANDIDATES.length);
});
it('sets the correct columns in the table', () => {
const expectedColumnNames = [
'Name',
'Created at',
- 'User',
+ 'Author',
'L1 Ratio',
'Rmse',
'Auc',
'Mae',
- '',
- '',
+ 'Artifacts',
];
expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
@@ -270,7 +215,9 @@ describe('MlExperiment', () => {
});
it('shows empty state when no artifact', () => {
- expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe('-');
+ expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe(
+ 'No artifacts',
+ );
});
});
@@ -301,15 +248,7 @@ describe('MlExperiment', () => {
});
it('when there is no user shows nothing', () => {
- expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('');
- });
- });
-
- describe('Detail column', () => {
- const detailColumn = -2;
-
- it('is a link to details', () => {
- expect(hrefInRowAndColumn(firstCandidateIndex, detailColumn)).toBe(firstCandidate.details);
+ expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('No name');
});
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
new file mode 100644
index 00000000000..66378cd3f0d
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
@@ -0,0 +1,52 @@
+export const MOCK_START_CURSOR = 'eyJpZCI6IjE2In0';
+
+export const MOCK_PAGE_INFO = {
+ startCursor: MOCK_START_CURSOR,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+};
+
+export const MOCK_CANDIDATES = [
+ {
+ rmse: 1,
+ l1_ratio: 0.4,
+ details: 'link_to_candidate1',
+ artifact: 'link_to_artifact',
+ name: 'aCandidate',
+ created_at: '2023-01-05T14:07:02.975Z',
+ user: { username: 'root', path: '/root' },
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate2',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate3',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate4',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate5',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+];
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index 0158966997f..cc38a3fd8a1 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -51,10 +51,6 @@ describe('Column component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('xAxisLabel', () => {
const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
index 484199698ea..33ea5e83598 100644
--- a/spec/frontend/monitoring/components/charts/gauge_spec.js
+++ b/spec/frontend/monitoring/components/charts/gauge_spec.js
@@ -21,11 +21,6 @@ describe('Gauge Chart component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('chart component', () => {
it('is rendered when props are passed', () => {
createWrapper();
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
index e163d4e73a0..54245cbdbc1 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -28,10 +28,6 @@ describe('Heatmap component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should display a label on the x axis', () => {
expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 62a0b7e6ad3..fa31b479296 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -21,10 +21,6 @@ describe('Single Stat Chart component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('statValue', () => {
it('should display the correct value', () => {
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 503dee7b937..c1b51f71a7e 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -58,10 +58,6 @@ describe('Time series component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('With a single time series', () => {
describe('general functions', () => {
const findChart = () => wrapper.findComponent({ ref: 'chart' });
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
index 88de3467580..eb05b1f184a 100644
--- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
@@ -29,10 +29,6 @@ describe('Create dashboard modal', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has button that links to the project url', async () => {
findRepoButton().trigger('click');
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index bb57420d406..2758103fd6e 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -55,11 +55,6 @@ describe('Actions menu', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('add metric item', () => {
it('is rendered when custom metrics are available', async () => {
createShallowWrapper();
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 18ccda2c41c..ab259249772 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -59,10 +59,6 @@ describe('Dashboard header', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('dashboards dropdown', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index d71f6374967..1cfd132b123 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -49,8 +49,6 @@ describe('dashboard invalid url parameters', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- afterEach(() => {});
-
it('is mounted', () => {
expect(wrapper.exists()).toBe(true);
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 339c1710a9e..491649e5b96 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -106,10 +106,6 @@ describe('Dashboard Panel', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the chart title', () => {
expect(findTitle().text()).toBe(graphDataEmpty.title);
});
@@ -134,10 +130,6 @@ describe('Dashboard Panel', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders no chart title', () => {
expect(findTitle().text()).toBe('');
});
@@ -160,10 +152,6 @@ describe('Dashboard Panel', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the chart title', () => {
expect(findTitle().text()).toBe(graphData.title);
});
@@ -377,10 +365,6 @@ describe('Dashboard Panel', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`;
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 1d17a9116df..1f995965003 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -4,7 +4,7 @@ import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { ESC_KEY } from '~/lib/utils/keys';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -33,7 +33,7 @@ import {
setupStoreWithLinks,
} from '../store_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Dashboard', () => {
let store;
@@ -75,7 +75,6 @@ describe('Dashboard', () => {
if (store.dispatch.mockReset) {
store.dispatch.mockReset();
}
- wrapper.destroy();
});
describe('request information to the server', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 9873654bdda..98791906700 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
queryToObject,
@@ -18,7 +18,7 @@ import { defaultTimeRange } from '~/vue_shared/constants';
import { dashboardProps } from '../fixture_data';
import { mockProjectDir } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('dashboard invalid url parameters', () => {
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 104263e73e0..593d832f297 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -18,10 +18,6 @@ describe('Graph group component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When group is not collapsed', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
index e3cd26b0e48..d3a48be7939 100644
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ b/spec/frontend/monitoring/components/group_empty_state_spec.js
@@ -23,10 +23,6 @@ function createComponent(props) {
describe('GroupEmptyState', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each([
metricStates.NO_DATA,
metricStates.TIMEOUT,
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index cb300870689..f6cc6789b1f 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -40,6 +40,7 @@ describe('RefreshButton', () => {
afterEach(() => {
dispatch.mockReset();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
});
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index 012e2e9c3e2..96b228fd3b2 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,6 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
@@ -56,11 +55,8 @@ describe('Custom variable component', () => {
it('changing dropdown items triggers update', async () => {
createShallowWrapper();
- jest.spyOn(wrapper.vm, '$emit');
-
findDropdownItems().at(1).vm.$emit('click');
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary');
+ expect(wrapper.emitted('input')).toEqual([['canary']]);
});
});
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index 3073b3968aa..20e1937c5ac 100644
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -33,25 +33,23 @@ describe('Text variable component', () => {
it('triggers keyup enter', async () => {
createShallowWrapper();
- jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'prod-pod';
findInput().trigger('input');
findInput().trigger('keyup.enter');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod');
+ expect(wrapper.emitted('input')).toEqual([['prod-pod']]);
});
it('triggers blur enter', async () => {
createShallowWrapper();
- jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'canary-pod';
findInput().trigger('input');
findInput().trigger('blur');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod');
+ expect(wrapper.emitted('input')).toEqual([['canary-pod']]);
});
});
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
index fa112fca2db..98ee6c1cb29 100644
--- a/spec/frontend/monitoring/pages/panel_new_page_spec.js
+++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js
@@ -49,10 +49,6 @@ describe('monitoring/pages/panel_new_page', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('back to dashboard button', () => {
it('is rendered', () => {
expect(findBackButton().exists()).toBe(true);
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 8eda46a2ff1..8097857f226 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
@@ -61,7 +61,7 @@ import {
mockDashboardsErrorResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Monitoring store actions', () => {
const { convertObjectPropsToCamelCase } = commonUtils;
@@ -177,7 +177,6 @@ describe('Monitoring store actions', () => {
});
it('dispatches when feature metricsDashboardAnnotations is on', () => {
- const origGon = window.gon;
window.gon = { features: { metricsDashboardAnnotations: true } };
return testAction(
@@ -190,9 +189,7 @@ describe('Monitoring store actions', () => {
{ type: 'fetchDashboard' },
{ type: 'fetchAnnotations' },
],
- ).then(() => {
- window.gon = origGon;
- });
+ );
});
});
@@ -263,7 +260,7 @@ describe('Monitoring store actions', () => {
});
});
- it('does not show a flash error when showErrorBanner is disabled', async () => {
+ it('does not show an alert error when showErrorBanner is disabled', async () => {
state.showErrorBanner = false;
await result();
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index bad24345f9d..fe543a346b5 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -1,16 +1,16 @@
import { mount, createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { getByText as getByTextHelper } from '@testing-library/dom';
-import { GlToggle } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlToggle } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_ENDPONT = 'https://example.com/toggle';
@@ -20,6 +20,7 @@ describe('NewNavToggle', () => {
let wrapper;
const findToggle = () => wrapper.findComponent(GlToggle);
+ const findDisclosureItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const createComponent = (propsData = { enabled: false }) => {
wrapper = mount(NewNavToggle, {
@@ -30,83 +31,156 @@ describe('NewNavToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
- it('renders its title', () => {
- createComponent();
- expect(getByText('Navigation redesign').exists()).toBe(true);
- });
+ describe('When rendered in scope of the new navigation', () => {
+ it('renders the disclosure item', () => {
+ createComponent({ newNavigation: true, enabled: true });
+ expect(findDisclosureItem().exists()).toBe(true);
+ });
- describe('when user preference is enabled', () => {
- beforeEach(() => {
- createComponent({ enabled: true });
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ newNavigation: true, enabled: true });
+ });
+
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
});
- it('renders the toggle as enabled', () => {
- expect(findToggle().props('value')).toBe(true);
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
+
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
+ });
+
+ describe.each`
+ desc | actFn
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
+ ${'on menu item action'} | ${() => findDisclosureItem().vm.$emit('action')}
+ `('$desc', ({ actFn }) => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent({ enabled: false, newNavigation: true });
+ });
+
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+
+ actFn();
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ actFn();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(true);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
});
});
- describe('when user preference is disabled', () => {
- beforeEach(() => {
- createComponent({ enabled: false });
+ describe('When rendered in scope of the current navigation', () => {
+ it('renders its title', () => {
+ createComponent();
+ expect(getByText('Navigation redesign').exists()).toBe(true);
});
- it('renders the toggle as disabled', () => {
- expect(findToggle().props('value')).toBe(false);
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: true });
+ });
+
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
});
- });
- describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
- `('$desc', ({ actFn }) => {
- let mock;
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
- beforeEach(() => {
- mock = new MockAdapter(axios);
- createComponent({ enabled: false });
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
});
- it('reloads the page on success', async () => {
- mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ describe.each`
+ desc | actFn
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
+ `('$desc', ({ actFn }) => {
+ let mock;
- actFn();
- await waitForPromises();
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent({ enabled: false });
+ });
- expect(window.location.reload).toHaveBeenCalled();
- });
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
- it('shows an alert on error', async () => {
- mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ actFn();
+ await waitForPromises();
- actFn();
- await waitForPromises();
+ expect(window.location.reload).toHaveBeenCalled();
+ });
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: s__(
- 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
- ),
- }),
- );
- expect(window.location.reload).not.toHaveBeenCalled();
- });
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- it('changes the toggle', async () => {
- await actFn();
+ actFn();
+ await waitForPromises();
- expect(findToggle().props('value')).toBe(true);
- });
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(true);
+ });
- afterEach(() => {
- mock.restore();
+ afterEach(() => {
+ mock.restore();
+ });
});
});
});
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
index 76b8ebdc92f..9d3b43520ec 100644
--- a/spec/frontend/nav/components/responsive_app_spec.js
+++ b/spec/frontend/nav/components/responsive_app_spec.js
@@ -33,10 +33,6 @@ describe('~/nav/components/responsive_app.vue', () => {
document.body.className = 'test-class';
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js
index f87de0afb14..2514035270a 100644
--- a/spec/frontend/nav/components/responsive_header_spec.js
+++ b/spec/frontend/nav/components/responsive_header_spec.js
@@ -14,7 +14,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
default: TEST_SLOT_CONTENT,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -25,10 +25,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders slot', () => {
expect(wrapper.text()).toBe(TEST_SLOT_CONTENT);
});
diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js
index 8f198d92747..5a5cfc93607 100644
--- a/spec/frontend/nav/components/responsive_home_spec.js
+++ b/spec/frontend/nav/components/responsive_home_spec.js
@@ -29,7 +29,7 @@ describe('~/nav/components/responsive_home.vue', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
listeners: {
'menu-item-click': menuItemClickListener,
@@ -45,10 +45,6 @@ describe('~/nav/components/responsive_home.vue', () => {
menuItemClickListener = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index e70f70afc97..7f39552eb42 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -28,10 +28,6 @@ describe('~/nav/components/top_nav_app.vue', () => {
const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle');
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponentShallow();
diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js
index 293fe361fa9..388ac243648 100644
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ b/spec/frontend/nav/components/top_nav_container_view_spec.js
@@ -48,10 +48,6 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
};
const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each(['projects', 'groups'])(
'emits frequent items event to event hub (%s)',
async (frequentItemsDropdownType) => {
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 8a0340087ec..08d6650b5bb 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -36,10 +36,6 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
active: idx === activeIndex,
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
});
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 7a5a8475ab7..7a3e58fd964 100644
--- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
@@ -54,10 +54,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems: findMenuItemModels(x),
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
index 18210658b89..2cd65307b0b 100644
--- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
+++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
@@ -1,6 +1,8 @@
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
const TEST_VIEW_MODEL = {
title: 'Dropdown',
@@ -18,6 +20,16 @@ const TEST_VIEW_MODEL = {
menu_items: [
{ id: 'bar-1', title: 'Bar 1', href: '/bar/1' },
{ id: 'bar-2', title: 'Bar 2', href: '/bar/2' },
+ {
+ id: 'invite',
+ title: '_invite members title_',
+ component: TOP_NAV_INVITE_MEMBERS_COMPONENT,
+ icon: '_icon_',
+ data: {
+ trigger_element: '_trigger_element_',
+ trigger_source: '_trigger_source_',
+ },
+ },
],
},
],
@@ -36,6 +48,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findDropdownContents = () =>
findDropdown()
.findAll('[data-testid]')
@@ -55,10 +68,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -73,6 +82,10 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
});
it('renders dropdown content', () => {
+ const hrefItems = TEST_VIEW_MODEL.menu_sections[1].menu_items.filter((item) =>
+ Boolean(item.href),
+ );
+
expect(findDropdownContents()).toEqual([
{
type: 'header',
@@ -90,12 +103,18 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
type: 'header',
text: TEST_VIEW_MODEL.menu_sections[1].title,
},
- ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({
+ ...hrefItems.map(({ title, href }) => ({
type: 'item',
href,
text: title,
})),
]);
+ expect(findInviteMembersTrigger().props()).toMatchObject({
+ displayText: '_invite members title_',
+ icon: '_icon_',
+ triggerElement: 'dropdown-_trigger_element_',
+ triggerSource: '_trigger_source_',
+ });
});
});
diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js
index 10762a1c3a2..9836400a366 100644
--- a/spec/frontend/notebook/cells/code_spec.js
+++ b/spec/frontend/notebook/cells/code_spec.js
@@ -13,10 +13,6 @@ describe('Code component', () => {
json = JSON.parse(JSON.stringify(fixture));
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without output', () => {
beforeEach(() => {
wrapper = mountComponent(json.cells[0]);
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index a7776bd5b69..f226776212a 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -1,18 +1,16 @@
import { mount } from '@vue/test-utils';
import katex from 'katex';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
import basicJson from 'test_fixtures/blob/notebook/basic.json';
import mathJson from 'test_fixtures/blob/notebook/math.json';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
import Prompt from '~/notebook/cells/prompt.vue';
-const Component = Vue.extend(MarkdownComponent);
-
window.katex = katex;
function buildCellComponent(cell, relativePath = '', hidePrompt) {
- return mount(Component, {
+ return mount(MarkdownComponent, {
propsData: {
cell,
hidePrompt,
diff --git a/spec/frontend/notebook/cells/output/error_spec.js b/spec/frontend/notebook/cells/output/error_spec.js
new file mode 100644
index 00000000000..2e4ca8c1761
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/error_spec.js
@@ -0,0 +1,48 @@
+import { mount } from '@vue/test-utils';
+import ErrorOutput from '~/notebook/cells/output/error.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
+import Markdown from '~/notebook/cells/markdown.vue';
+import { errorOutputContent, relativeRawPath } from '../../mock_data';
+
+describe('notebook/cells/output/error.vue', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(ErrorOutput, {
+ propsData: {
+ rawCode: errorOutputContent,
+ index: 1,
+ count: 2,
+ },
+ provide: { relativeRawPath },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findPrompt = () => wrapper.findComponent(Prompt);
+ const findMarkdown = () => wrapper.findComponent(Markdown);
+
+ it('renders the prompt', () => {
+ expect(findPrompt().props()).toMatchObject({ count: 2, showOutput: true, type: 'Out' });
+ });
+
+ it('renders the markdown', () => {
+ const expectedParsedMarkdown =
+ '```error\n' +
+ '---------------------------------------------------------------------------\n' +
+ 'NameError Traceback (most recent call last)\n' +
+ '/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py in <module>\n' +
+ '----> 1 To\n' +
+ '\n' +
+ "NameError: name 'To' is not defined\n" +
+ '```';
+
+ expect(findMarkdown().props()).toMatchObject({
+ cell: { source: [expectedParsedMarkdown] },
+ hidePrompt: true,
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 585cbb68eeb..1241c133b89 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -17,10 +17,6 @@ describe('Output component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('text output', () => {
beforeEach(() => {
const textType = json.cells[2];
diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js
index 0cda0c5bc2b..4c864a9b930 100644
--- a/spec/frontend/notebook/cells/prompt_spec.js
+++ b/spec/frontend/notebook/cells/prompt_spec.js
@@ -6,10 +6,6 @@ describe('Prompt component', () => {
const mountComponent = ({ type }) => shallowMount(Prompt, { propsData: { type, count: 1 } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('input', () => {
beforeEach(() => {
wrapper = mountComponent({ type: 'In' });
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index b79000a3505..3c73d420703 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -1,16 +1,14 @@
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import json from 'test_fixtures/blob/notebook/basic.json';
import jsonWithWorksheet from 'test_fixtures/blob/notebook/worksheets.json';
import Notebook from '~/notebook/index.vue';
-const Component = Vue.extend(Notebook);
-
describe('Notebook component', () => {
let vm;
function buildComponent(notebook) {
- return mount(Component, {
+ return mount(Notebook, {
propsData: { notebook },
provide: { relativeRawPath: '' },
}).vm;
diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js
index b1419e1256f..5c47cb5aa9b 100644
--- a/spec/frontend/notebook/mock_data.js
+++ b/spec/frontend/notebook/mock_data.js
@@ -1,2 +1,8 @@
export const relativeRawPath = '/test';
export const markdownCellContent = ['# Test'];
+export const errorOutputContent = [
+ '\u001b[0;31m---------------------------------------------------------------------------\u001b[0m',
+ '\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)',
+ '\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mTo\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m',
+ "\u001b[0;31mNameError\u001b[0m: name 'To' is not defined",
+];
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index dfb05c85fc8..062cd098640 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -5,11 +5,12 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Autosave from '~/autosave';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import axios from '~/lib/utils/axios_utils';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import CommentForm from '~/notes/components/comment_form.vue';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
@@ -21,8 +22,7 @@ import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock }
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
-jest.mock('~/flash');
-jest.mock('~/autosave');
+jest.mock('~/alert');
Vue.use(Vuex);
@@ -32,7 +32,8 @@ describe('issue_comment_form component', () => {
let axiosMock;
const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
- const findTextArea = () => wrapper.findByTestId('comment-field');
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findMarkdownEditorTextarea = () => findMarkdownEditor().find('textarea');
const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button');
const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button');
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('internal-note-checkbox');
@@ -127,7 +128,6 @@ describe('issue_comment_form component', () => {
afterEach(() => {
axiosMock.restore();
- wrapper.destroy();
});
describe('user is logged in', () => {
@@ -136,7 +136,6 @@ describe('issue_comment_form component', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- jest.spyOn(wrapper.vm, 'resizeTextarea');
jest.spyOn(wrapper.vm, 'stopPolling');
findCloseReopenButton().trigger('click');
@@ -145,7 +144,6 @@ describe('issue_comment_form component', () => {
expect(wrapper.vm.note).toBe('');
expect(wrapper.vm.saveNote).toHaveBeenCalled();
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
- expect(wrapper.vm.resizeTextarea).toHaveBeenCalled();
});
it('does not report errors in the UI when the save succeeds', async () => {
@@ -260,6 +258,18 @@ describe('issue_comment_form component', () => {
});
});
+ it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
+ mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } });
+
+ expect(wrapper.text()).not.toContain('Rich text');
+ });
+
+ it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
+ mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
+
+ expect(wrapper.text()).toContain('Rich text');
+ });
+
describe('textarea', () => {
describe('general', () => {
it.each`
@@ -268,13 +278,13 @@ describe('issue_comment_form component', () => {
${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
`(
'should render textarea with placeholder for $noteType',
- ({ noteIsInternal, placeholder }) => {
- mountComponent({
- mountFunction: mount,
- initialData: { noteIsInternal },
- });
+ async ({ noteIsInternal, placeholder }) => {
+ mountComponent();
+
+ wrapper.vm.noteIsInternal = noteIsInternal;
+ await nextTick();
- expect(findTextArea().attributes('placeholder')).toBe(placeholder);
+ expect(findMarkdownEditor().props('formFieldProps').placeholder).toBe(placeholder);
},
);
@@ -290,13 +300,13 @@ describe('issue_comment_form component', () => {
await findCommentButton().trigger('click');
- expect(findTextArea().attributes('disabled')).toBe('disabled');
+ expect(findMarkdownEditor().find('textarea').attributes('disabled')).toBe('disabled');
});
it('should support quick actions', () => {
mountComponent({ mountFunction: mount });
- expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true');
+ expect(findMarkdownEditor().props('supportsQuickActions')).toBe(true);
});
it('should link to markdown docs', () => {
@@ -336,63 +346,51 @@ describe('issue_comment_form component', () => {
it('should enter edit mode when arrow up is pressed', () => {
jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
- findTextArea().trigger('keydown.up');
+ findMarkdownEditorTextarea().trigger('keydown.up');
expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled();
});
- it('inits autosave', () => {
- expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
- 'Note',
- 'Issue',
- noteableDataMock.id,
- ]);
- });
- });
+ describe('event enter', () => {
+ describe('when no draft exists', () => {
+ it('should save note when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
- describe('event enter', () => {
- beforeEach(() => {
- mountComponent({ mountFunction: mount });
- });
-
- describe('when no draft exists', () => {
- it('should save note when cmd+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSave');
-
- findTextArea().trigger('keydown.enter', { metaKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
- expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
- });
+ expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ });
- it('should save note when ctrl+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSave');
+ it('should save note when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
- findTextArea().trigger('keydown.enter', { ctrlKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
- expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ });
});
- });
- describe('when a draft exists', () => {
- beforeEach(() => {
- store.registerModule('batchComments', batchComments());
- store.state.batchComments.drafts = [{ note: 'A' }];
- });
+ describe('when a draft exists', () => {
+ beforeEach(() => {
+ store.registerModule('batchComments', batchComments());
+ store.state.batchComments.drafts = [{ note: 'A' }];
+ });
- it('should save note draft when cmd+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSaveDraft');
+ it('should save note draft when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSaveDraft');
- findTextArea().trigger('keydown.enter', { metaKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
- expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
- });
+ expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ });
- it('should save note draft when ctrl+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSaveDraft');
+ it('should save note draft when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSaveDraft');
- findTextArea().trigger('keydown.enter', { ctrlKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
- expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ });
});
});
});
@@ -482,7 +480,7 @@ describe('issue_comment_form component', () => {
it(`makes an API call to open it`, () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.OPENED },
+ noteableData: { ...noteableDataMock, state: STATUS_OPEN },
mountFunction: mount,
});
@@ -496,7 +494,7 @@ describe('issue_comment_form component', () => {
it(`shows an error when the API call fails`, async () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.OPENED },
+ noteableData: { ...noteableDataMock, state: STATUS_OPEN },
mountFunction: mount,
});
@@ -517,7 +515,7 @@ describe('issue_comment_form component', () => {
it('makes an API call to close it', () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ noteableData: { ...noteableDataMock, state: STATUS_CLOSED },
mountFunction: mount,
});
@@ -532,7 +530,7 @@ describe('issue_comment_form component', () => {
it(`shows an error when the API call fails`, async () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ noteableData: { ...noteableDataMock, state: STATUS_CLOSED },
mountFunction: mount,
});
@@ -661,7 +659,7 @@ describe('issue_comment_form component', () => {
});
it('should not render submission form', () => {
- expect(findTextArea().exists()).toBe(false);
+ expect(findMarkdownEditor().exists()).toBe(false);
});
});
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
index cabf551deba..b891c1f553d 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -24,10 +24,6 @@ describe('CommentTypeDropdown component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
isInternalNote | buttonText
${false} | ${COMMENT_FORM.comment}
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index bb44563b87a..66b86ed3ce0 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -22,10 +22,6 @@ describe('diff_discussion_header component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Avatar', () => {
const firstNoteAuthor = discussionMock.notes[0].author;
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index e414ada1854..a9a20bd8bc3 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -38,15 +38,12 @@ describe('DiscussionActions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
const createComponent = createComponentFactory();
it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => {
createComponent();
+
expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(true);
expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(true);
@@ -94,17 +91,15 @@ describe('DiscussionActions', () => {
it('emits showReplyForm event when clicking on reply placeholder', () => {
createComponent({}, { attachTo: document.body });
- jest.spyOn(wrapper.vm, '$emit');
wrapper.findComponent(ReplyPlaceholder).find('textarea').trigger('focus');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm');
+ expect(wrapper.emitted().showReplyForm).toHaveLength(1);
});
it('emits resolve event when clicking on resolve button', () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
wrapper.findComponent(ResolveDiscussionButton).find('button').trigger('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve');
+ expect(wrapper.emitted().resolve).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index f4ec7f835bb..ac677841ee1 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -40,7 +40,6 @@ describe('DiscussionCounter component', () => {
afterEach(() => {
wrapper.vm.$destroy();
- wrapper = null;
});
describe('has no discussions', () => {
@@ -119,8 +118,6 @@ describe('DiscussionCounter component', () => {
toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]');
};
- afterEach(() => wrapper.destroy());
-
it('calls button handler when clicked', async () => {
await updateStoreWithExpanded(true);
diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js
index 48f5030aa1a..e31155a028f 100644
--- a/spec/frontend/notes/components/discussion_filter_note_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_note_spec.js
@@ -18,11 +18,6 @@ describe('DiscussionFilterNote component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('timelineContent renders a string containing instruction for switching feed type', () => {
expect(wrapper.find('[data-testid="discussion-filter-timeline-content"]').html()).toBe(
'<div data-testid="discussion-filter-timeline-content">You\'re only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.</div>',
diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js
index 77ae7b2c3b5..6e095f63003 100644
--- a/spec/frontend/notes/components/discussion_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_navigator_spec.js
@@ -37,7 +37,6 @@ describe('notes/components/discussion_navigator', () => {
if (wrapper) {
wrapper.destroy();
}
- wrapper = null;
});
describe('on create', () => {
diff --git a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js
index 8d5ea108b50..d11ca7ad1ec 100644
--- a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js
@@ -19,10 +19,6 @@ describe('DiscussionNotesRepliesWrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when normal discussion', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index add2ed1ba8a..bc0c04f2d8a 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -53,11 +53,6 @@ describe('DiscussionNotes', () => {
store.dispatch('setNotesData', notesDataMock);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('rendering', () => {
it('renders an element for each note in the discussion', () => {
createComponent();
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 971e3987929..a9201b78669 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -17,10 +17,6 @@ describe('ReplyPlaceholder', () => {
const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits focus event on button click', async () => {
createComponent({ options: { attachTo: document.body } });
diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index 17c3523cf48..4bd21842fec 100644
--- a/spec/frontend/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
@@ -23,10 +23,6 @@ describe('resolveDiscussionButton', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should emit a onClick event on button click', async () => {
const button = wrapper.findComponent(GlButton);
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 a185f11ffaa..3dfae45ec49 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
@@ -15,10 +15,6 @@ describe('ResolveWithIssueButton', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should have a link with the provided link property as href', () => {
const button = wrapper.findComponent(GlButton);
diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js
index ab1a6b152a4..34b7524d8fb 100644
--- a/spec/frontend/notes/components/email_participants_warning_spec.js
+++ b/spec/frontend/notes/components/email_participants_warning_spec.js
@@ -4,11 +4,6 @@ import EmailParticipantsWarning from '~/notes/components/email_participants_warn
describe('Email Participants Warning Component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findMoreButton = () => wrapper.find('button');
const createWrapper = (emails) => {
diff --git a/spec/frontend/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js
index 20b32b8c178..68b11fb3b1a 100644
--- a/spec/frontend/notes/components/note_actions/reply_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js
@@ -9,11 +9,6 @@ describe('ReplyButton', () => {
wrapper = shallowMount(ReplyButton);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('emits startReplying on click', () => {
wrapper.findComponent(GlButton).vm.$emit('click');
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
index 658e844a9b1..bee08ee0605 100644
--- a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
@@ -20,10 +20,6 @@ describe('NoteTimelineEventButton', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTimelineButton = () => wrapper.findComponent(GlButton);
it('emits click-promote-comment-to-event', async () => {
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 8630b7b7d07..63286927d53 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -77,7 +77,6 @@ describe('noteActions', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -203,7 +202,6 @@ describe('noteActions', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -226,7 +224,6 @@ describe('noteActions', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -248,10 +245,6 @@ describe('noteActions', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not be possible to assign the comment author', testButtonDoesNotRender);
it('should not be possible to unassign the comment author', testButtonDoesNotRender);
});
diff --git a/spec/frontend/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js
index 24632f8e427..7f44171f6cc 100644
--- a/spec/frontend/notes/components/note_attachment_spec.js
+++ b/spec/frontend/notes/components/note_attachment_spec.js
@@ -15,11 +15,6 @@ describe('Issue note attachment', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders attachment image if it is passed in attachment prop', () => {
createComponent({
image: 'test-image',
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index c71cf7666ab..b4f185004bb 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -49,10 +49,6 @@ describe('issue_note_body component', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the note', () => {
expect(wrapper.find('.note-text').html()).toContain(note.note_html);
});
diff --git a/spec/frontend/notes/components/note_edited_text_spec.js b/spec/frontend/notes/components/note_edited_text_spec.js
index 0a5fe48ef94..577e1044588 100644
--- a/spec/frontend/notes/components/note_edited_text_spec.js
+++ b/spec/frontend/notes/components/note_edited_text_spec.js
@@ -1,3 +1,4 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import NoteEditedText from '~/notes/components/note_edited_text.vue';
@@ -5,41 +6,63 @@ const propsData = {
actionText: 'Edited',
className: 'foo-bar',
editedAt: '2017-08-04T09:52:31.062Z',
- editedBy: {
- avatar_url: 'path',
- id: 1,
- name: 'Root',
- path: '/root',
- state: 'active',
- username: 'root',
- },
+ editedBy: null,
};
describe('NoteEditedText', () => {
let wrapper;
- beforeEach(() => {
+ const createWrapper = (props = {}) => {
wrapper = shallowMount(NoteEditedText, {
- propsData,
+ propsData: {
+ ...propsData,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
});
- });
+ };
- afterEach(() => {
- wrapper.destroy();
- });
+ const findUserElement = () => wrapper.findComponent(GlLink);
- it('should render block with provided className', () => {
- expect(wrapper.classes()).toContain(propsData.className);
- });
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
- it('should render provided actionText', () => {
- expect(wrapper.text().trim()).toContain(propsData.actionText);
+ it('should render block with provided className', () => {
+ expect(wrapper.classes()).toContain(propsData.className);
+ });
+
+ it('should render provided actionText', () => {
+ expect(wrapper.text().trim()).toContain(propsData.actionText);
+ });
+
+ it('should not render user information', () => {
+ expect(findUserElement().exists()).toBe(false);
+ });
});
- it('should render provided user information', () => {
- const authorLink = wrapper.find('.js-user-link');
+ describe('edited note', () => {
+ const editedBy = {
+ avatar_url: 'path',
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+ };
+
+ beforeEach(() => {
+ createWrapper({ editedBy });
+ });
+
+ it('should render user information', () => {
+ const authorLink = findUserElement();
- expect(authorLink.attributes('href')).toEqual(propsData.editedBy.path);
- expect(authorLink.text().trim()).toEqual(propsData.editedBy.name);
+ expect(authorLink.attributes('href')).toEqual(editedBy.path);
+ expect(authorLink.text().trim()).toEqual(editedBy.name);
+ });
});
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 90473e7ccba..59362e18098 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -48,10 +48,6 @@ describe('issue_note_form component', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('noteHash', () => {
beforeEach(() => {
wrapper = createComponentWrapper();
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 56c22b09e1b..b3d6fab7f91 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -44,11 +44,6 @@ describe('NoteHeader component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('does not render discussion actions when includeToggle is false', () => {
createComponent({
includeToggle: false,
diff --git a/spec/frontend/notes/components/note_signed_out_widget_spec.js b/spec/frontend/notes/components/note_signed_out_widget_spec.js
index 84f20e4ad58..d56ee234cd9 100644
--- a/spec/frontend/notes/components/note_signed_out_widget_spec.js
+++ b/spec/frontend/notes/components/note_signed_out_widget_spec.js
@@ -12,10 +12,6 @@ describe('NoteSignedOutWidget component', () => {
wrapper = shallowMount(NoteSignedOutWidget, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders sign in link provided in the store', () => {
expect(wrapper.find(`a[href="${notesDataMock.newSessionPath}"]`).text()).toBe('sign in');
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index a90d8bdde06..ac0c037fe36 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -22,7 +22,6 @@ jest.mock('~/behaviors/markdown/render_gfm');
describe('noteable_discussion component', () => {
let store;
let wrapper;
- let originalGon;
beforeEach(() => {
window.mrTabs = {};
@@ -36,10 +35,6 @@ describe('noteable_discussion component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not render thread header for non diff threads', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
@@ -167,16 +162,6 @@ describe('noteable_discussion component', () => {
});
describe('signout widget', () => {
- beforeEach(() => {
- originalGon = { ...window.gon };
- window.gon = window.gon || {};
- });
-
- afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
- });
-
describe('user is logged in', () => {
beforeEach(() => {
window.gon.current_user_id = userDataMock.id;
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index af1b4f64037..b158cfff10d 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -71,10 +71,6 @@ describe('issue_note', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mutiline comments', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/notes/components/notes_activity_header_spec.js b/spec/frontend/notes/components/notes_activity_header_spec.js
index 5b3165bf401..2de491477b6 100644
--- a/spec/frontend/notes/components/notes_activity_header_spec.js
+++ b/spec/frontend/notes/components/notes_activity_header_spec.js
@@ -24,10 +24,6 @@ describe('~/notes/components/notes_activity_header.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index b08a22f8674..832264aa7d3 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -90,8 +90,9 @@ describe('note_app', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
});
describe('render', () => {
diff --git a/spec/frontend/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js
index 8c3696e88b7..ef5f06ad2fa 100644
--- a/spec/frontend/notes/components/toggle_replies_widget_spec.js
+++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js
@@ -30,10 +30,6 @@ describe('toggle replies widget for notes', () => {
const mountComponent = ({ collapsed = false }) =>
mountExtended(ToggleRepliesWidget, { propsData: { replies, collapsed } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('collapsed state', () => {
beforeEach(() => {
wrapper = mountComponent({ collapsed: true });
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 6d3bc19bd45..40f10ca901b 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -23,7 +23,6 @@ const fixture = 'snippets/show.html';
let mockAxios;
window.project_uploads_path = `${TEST_HOST}/uploads`;
-window.gon = window.gon || {};
window.gl = window.gl || {};
gl.utils = gl.utils || {};
gl.utils.disableButtonIfEmptyField = () => {};
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index c4c0dc58b0d..0d3ebea7af2 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -3,7 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
@@ -36,7 +36,7 @@ import {
const TEST_ERROR_MESSAGE = 'Test error message';
const mockAlertDismiss = jest.fn();
-jest.mock('~/flash', () => ({
+jest.mock('~/alert', () => ({
createAlert: jest.fn().mockImplementation(() => ({
dismiss: mockAlertDismiss,
})),
@@ -876,7 +876,7 @@ describe('Actions Notes Store', () => {
const res = { errors: { base: ['something went wrong'] } };
const error = { message: 'Unprocessable entity', response: { data: res } };
- it('sets flash alert using errors.base message', async () => {
+ it('sets an alert using errors.base message', async () => {
const resp = await actions.saveNote(
{
commit() {},
@@ -906,6 +906,20 @@ describe('Actions Notes Store', () => {
expect(data).toBe(res);
expect(createAlert).not.toHaveBeenCalled();
});
+
+ it('dispatches clearDrafts is command names contains submit_review', async () => {
+ const response = { command_names: ['submit_review'], valid: true };
+ dispatch = jest.fn().mockResolvedValue(response);
+ await actions.saveNote(
+ {
+ commit() {},
+ dispatch,
+ },
+ payload,
+ );
+
+ expect(dispatch).toHaveBeenCalledWith('batchComments/clearDrafts');
+ });
});
});
@@ -946,7 +960,7 @@ describe('Actions Notes Store', () => {
});
});
- it('when service fails, flashes error message', () => {
+ it('when service fails, creates an alert with error message', () => {
const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
Api.applySuggestion.mockReturnValue(Promise.reject(response));
@@ -1439,10 +1453,6 @@ describe('Actions Notes Store', () => {
describe('fetchDiscussions', () => {
const discussion = { notes: [] };
- afterEach(() => {
- window.gon = {};
- });
-
it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', () => {
axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion });
return testAction(
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 70749557e61..0fbd073191e 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -2,7 +2,6 @@ import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from '
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -66,8 +65,6 @@ describe('CustomNotificationsModal', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
@@ -87,24 +84,23 @@ describe('CustomNotificationsModal', () => {
describe('checkbox items', () => {
beforeEach(async () => {
+ const endpointUrl = '/api/v4/notification_settings';
+
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
+
wrapper = createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- events: [
- { id: 'new_release', enabled: true, name: 'New release', loading: false },
- { id: 'new_note', enabled: false, name: 'New note', loading: true },
- ],
- });
+ wrapper.findComponent(GlModal).vm.$emit('show');
- await nextTick();
+ await waitForPromises();
});
it.each`
index | eventId | eventName | enabled | loading
${0} | ${'new_release'} | ${'New release'} | ${true} | ${false}
- ${1} | ${'new_note'} | ${'New note'} | ${false} | ${true}
+ ${1} | ${'new_note'} | ${'New note'} | ${false} | ${false}
`(
'renders a checkbox for "$eventName" with checked=$enabled',
async ({ index, eventName, enabled, loading }) => {
@@ -214,16 +210,9 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent({ injectedProperties });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- events: [
- { id: 'new_release', enabled: true, name: 'New release', loading: false },
- { id: 'new_note', enabled: false, name: 'New note', loading: false },
- ],
- });
+ wrapper.findComponent(GlModal).vm.$emit('show');
- await nextTick();
+ await waitForPromises();
findCheckboxAt(1).vm.$emit('change', true);
@@ -241,19 +230,18 @@ describe('CustomNotificationsModal', () => {
);
it('shows a toast message when the request fails', async () => {
- mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {});
+ const endpointUrl = '/api/v4/notification_settings';
+
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
+
+ mockAxios.onPut(endpointUrl).reply(HTTP_STATUS_NOT_FOUND, {});
wrapper = createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- events: [
- { id: 'new_release', enabled: true, name: 'New release', loading: false },
- { id: 'new_note', enabled: false, name: 'New note', loading: false },
- ],
- });
+ wrapper.findComponent(GlModal).vm.$emit('show');
- await nextTick();
+ await waitForPromises();
findCheckboxAt(1).vm.$emit('change', true);
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index 0f13de0e6d8..bae9b028cf7 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -25,7 +25,7 @@ describe('NotificationsDropdown', () => {
CustomNotificationsModal,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
provide: {
dropdownItems: mockDropdownItems,
@@ -61,8 +61,6 @@ describe('NotificationsDropdown', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
diff --git a/spec/frontend/observability/index_spec.js b/spec/frontend/observability/index_spec.js
new file mode 100644
index 00000000000..83f72ff72b5
--- /dev/null
+++ b/spec/frontend/observability/index_spec.js
@@ -0,0 +1,64 @@
+import { createWrapper } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import renderObservability from '~/observability/index';
+import ObservabilityApp from '~/observability/components/observability_app.vue';
+import { SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
+
+describe('renderObservability', () => {
+ let element;
+ let vueInstance;
+ let component;
+
+ const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
+ const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ element.setAttribute('id', 'js-observability-app');
+ element.dataset.observabilityIframeSrc = 'https://observe.gitlab.com/';
+ document.body.appendChild(element);
+
+ vueInstance = renderObservability();
+ component = createWrapper(vueInstance).findComponent(ObservabilityApp);
+ });
+
+ afterEach(() => {
+ element.remove();
+ });
+
+ it('should return a Vue instance', () => {
+ expect(vueInstance).toEqual(expect.any(Vue));
+ });
+
+ it('should render the ObservabilityApp component', () => {
+ expect(component.props('observabilityIframeSrc')).toBe('https://observe.gitlab.com/');
+ });
+
+ describe('skeleton variant', () => {
+ it.each`
+ pathDescription | path | variant
+ ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]}
+ ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]}
+ ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]}
+ ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]}
+ `(
+ 'renders the $variant skeleton variant for $pathDescription path',
+ async ({ path, variant }) => {
+ component.vm.$router.push(path);
+ await nextTick();
+
+ expect(component.props('skeletonVariant')).toBe(variant);
+ },
+ );
+ });
+
+ it('handle route-update events', async () => {
+ component.vm.$router.push('/something?foo=bar');
+ component.vm.$emit('route-update', { url: '/some_path' });
+ expect(component.vm.$router.currentRoute.path).toBe('/something');
+ expect(component.vm.$router.currentRoute.query).toEqual({
+ foo: 'bar',
+ observability_path: '/some_path',
+ });
+ });
+});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index e3bcd140d60..4a9be71b880 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -1,19 +1,20 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilityApp from '~/observability/components/observability_app.vue';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
-
-import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
+import {
+ MESSAGE_EVENT_TYPE,
+ INLINE_EMBED_DIMENSIONS,
+ FULL_APP_DIMENSIONS,
+ SKELETON_VARIANT_EMBED,
+} from '~/observability/constants';
import { darkModeEnabled } from '~/lib/utils/color_utils';
jest.mock('~/lib/utils/color_utils');
-describe('Observability root app', () => {
+describe('ObservabilityApp', () => {
let wrapper;
- const replace = jest.fn();
- const $router = {
- replace,
- };
+
const $route = {
pathname: 'https://gitlab.com/gitlab-org/',
path: 'https://gitlab.com/gitlab-org/-/observability/dashboards',
@@ -26,21 +27,19 @@ describe('Observability root app', () => {
const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
- const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
-
- const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+ const TEST_USERNAME = 'test-user';
- const mountComponent = (route = $route) => {
+ const mountComponent = (props) => {
wrapper = shallowMountExtended(ObservabilityApp, {
propsData: {
observabilityIframeSrc: TEST_IFRAME_SRC,
+ ...props,
},
stubs: {
'observability-skeleton': ObservabilitySkeleton,
},
mocks: {
- $router,
- $route: route,
+ $route,
},
});
};
@@ -48,17 +47,11 @@ describe('Observability root app', () => {
const dispatchMessageEvent = (message) =>
window.dispatchEvent(new MessageEvent('message', message));
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ gon.current_username = TEST_USERNAME;
});
describe('iframe src', () => {
- const TEST_USERNAME = 'test-user';
-
- beforeAll(() => {
- gon.current_username = TEST_USERNAME;
- });
-
it('should render an iframe with observabilityIframeSrc, decorated with light theme and username', () => {
darkModeEnabled.mockReturnValueOnce(false);
mountComponent();
@@ -92,48 +85,70 @@ describe('Observability root app', () => {
});
});
- describe('on GOUI_ROUTE_UPDATE', () => {
- it('should not call replace method from vue router if message event does not have url', () => {
- mountComponent();
- dispatchMessageEvent({
- type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE,
- payload: { data: 'some other data' },
+ describe('iframe kiosk query param', () => {
+ it('when inlineEmbed, it should set the proper kiosk query parameter', () => {
+ mountComponent({
+ inlineEmbed: true,
});
- expect(replace).not.toHaveBeenCalled();
+
+ const iframe = findIframe();
+
+ expect(iframe.attributes('src')).toBe(
+ `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}&kiosk=inline-embed`,
+ );
});
+ });
- it.each`
- condition | origin | observability_path | url
- ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
- ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
- `(
- 'should not call replace method from vue router if $condition',
- async ({ origin, observability_path, url }) => {
- mountComponent({ ...$route, query: { observability_path } });
- dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url } },
- origin,
- });
- expect(replace).not.toHaveBeenCalled();
- },
- );
+ describe('iframe size', () => {
+ it('should set the specified size', () => {
+ mountComponent({
+ height: INLINE_EMBED_DIMENSIONS.HEIGHT,
+ width: INLINE_EMBED_DIMENSIONS.WIDTH,
+ });
+
+ const iframe = findIframe();
+
+ expect(iframe.attributes('width')).toBe(INLINE_EMBED_DIMENSIONS.WIDTH);
+ expect(iframe.attributes('height')).toBe(INLINE_EMBED_DIMENSIONS.HEIGHT);
+ });
+
+ it('should fallback to default size', () => {
+ mountComponent({});
+
+ const iframe = findIframe();
- it('should call replace method from vue router on message event callback', () => {
+ expect(iframe.attributes('width')).toBe(FULL_APP_DIMENSIONS.WIDTH);
+ expect(iframe.attributes('height')).toBe(FULL_APP_DIMENSIONS.HEIGHT);
+ });
+ });
+
+ describe('skeleton variant', () => {
+ it('sets the specified skeleton variant', () => {
+ mountComponent({ skeletonVariant: SKELETON_VARIANT_EMBED });
+ const props = wrapper.findComponent(ObservabilitySkeleton).props();
+
+ expect(props.variant).toBe(SKELETON_VARIANT_EMBED);
+ });
+
+ it('should have a default skeleton variant', () => {
+ mountComponent();
+ const props = wrapper.findComponent(ObservabilitySkeleton).props();
+
+ expect(props.variant).toBe('dashboards');
+ });
+ });
+
+ describe('on GOUI_ROUTE_UPDATE', () => {
+ it('should emit a route-update event', () => {
mountComponent();
+ const payload = { url: '/explore' };
dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload },
origin: 'https://observe.gitlab.com',
});
- expect(replace).toHaveBeenCalled();
- expect(replace).toHaveBeenCalledWith({
- name: 'https://gitlab.com/gitlab-org/',
- query: {
- otherQuery: 100,
- observability_path: '/explore',
- },
- });
+ expect(wrapper.emitted('route-update')[0]).toEqual([payload]);
});
});
@@ -167,34 +182,17 @@ describe('Observability root app', () => {
});
});
- describe('skeleton variant', () => {
- it.each`
- pathDescription | path | variant
- ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]}
- ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]}
- ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]}
- ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]}
- `('renders the $variant skeleton variant for $pathDescription path', ({ path, variant }) => {
- mountComponent({ ...$route, path });
- const props = wrapper.findComponent(ObservabilitySkeleton).props();
-
- expect(props.variant).toBe(variant);
- });
- });
-
- describe('on observability ui unmount', () => {
- it('should remove message event and should not call replace method from vue router', () => {
+ describe('on unmount', () => {
+ it('should not emit any even on route update', () => {
mountComponent();
wrapper.destroy();
- // testing event cleanup logic, should not call on messege event after component is destroyed
-
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
origin: 'https://observe.gitlab.com',
});
- expect(replace).not.toHaveBeenCalled();
+ expect(wrapper.emitted('route-update')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
index a95597d8516..65dbb003743 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -6,8 +6,13 @@ import Skeleton from '~/observability/components/skeleton/index.vue';
import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue';
import ExploreSkeleton from '~/observability/components/skeleton/explore.vue';
import ManageSkeleton from '~/observability/components/skeleton/manage.vue';
+import EmbedSkeleton from '~/observability/components/skeleton/embed.vue';
-import { SKELETON_VARIANTS_BY_ROUTE, DEFAULT_TIMERS } from '~/observability/constants';
+import {
+ SKELETON_VARIANTS_BY_ROUTE,
+ DEFAULT_TIMERS,
+ SKELETON_VARIANT_EMBED,
+} from '~/observability/constants';
describe('Skeleton component', () => {
let wrapper;
@@ -22,6 +27,8 @@ describe('Skeleton component', () => {
const findManageSkeleton = () => wrapper.findComponent(ManageSkeleton);
+ const findEmbedSkeleton = () => wrapper.findComponent(EmbedSkeleton);
+
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ ...props } = {}) => {
@@ -97,16 +104,20 @@ describe('Skeleton component', () => {
${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANTS[0]}
${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
+ ${'embed'} | ${'variant is embed'} | ${SKELETON_VARIANT_EMBED}
${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
`('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
mountComponent({ variant });
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
- const showsDefaultSkeleton = !SKELETON_VARIANTS.includes(variant);
+ const showsDefaultSkeleton = ![...SKELETON_VARIANTS, SKELETON_VARIANT_EMBED].includes(
+ variant,
+ );
expect(findDashboardsSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[0]);
expect(findExploreSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[1]);
expect(findManageSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[2]);
+ expect(findEmbedSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANT_EMBED);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 732dfdd42fb..ee450dfc851 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -2,7 +2,7 @@ import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitla
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { timezones } from '~/monitoring/format_date';
@@ -13,7 +13,7 @@ import MetricsSettings from '~/operation_settings/components/metrics_settings.vu
import store from '~/operation_settings/store';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('operation settings external dashboard component', () => {
let wrapper;
@@ -198,7 +198,7 @@ describe('operation settings external dashboard component', () => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates flash banner on error', async () => {
+ it('creates alert banner on error', async () => {
mountComponent(false);
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
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 ff11c8843bb..8ba7e40d728 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
@@ -27,11 +27,6 @@ describe('delete_button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('tooltip', () => {
it('the title is controlled by tooltipTitle prop', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
index 620c96e8c9e..5a7cbdcff5b 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
@@ -46,11 +46,6 @@ describe('Delete Image', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('executes apollo mutate on doDelete', () => {
const mutate = jest.fn().mockResolvedValue({});
mountComponent({ mutate });
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 d45b993b5a2..9d187439ca3 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
@@ -19,11 +19,6 @@ describe('Delete alert', () => {
wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData });
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when deleteAlertType is null', () => {
it('does not show the alert', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
index 16c9485e69e..860f9b3a0c1 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
@@ -30,15 +30,10 @@ describe('Delete Modal', () => {
const expectPrimaryActionStatus = (disabled = true) =>
expect(findModal().props('actionPrimary')).toMatchObject(
expect.objectContaining({
- attributes: [{ variant: 'danger' }, { disabled }],
+ attributes: { variant: 'danger', disabled },
}),
);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('contains a GlModal', () => {
mountComponent();
expect(findModal().exists()).toBe(true);
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 b37edac83f7..9e443234c34 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
@@ -73,7 +73,7 @@ describe('Details Header', () => {
apolloProvider,
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
TitleArea,
@@ -85,9 +85,7 @@ describe('Details Header', () => {
afterEach(() => {
// if we want to mix createMockApollo and manual mocks we need to reset everything
- wrapper.destroy();
apolloProvider = undefined;
- wrapper = null;
});
describe('image name', () => {
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 ce5ecfe4608..d6c1b2c3f51 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
@@ -23,11 +23,6 @@ describe('Partial Cleanup alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it(`gl-alert has the correct properties`, () => {
mountComponent();
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 d83a5099bcd..3e1fd14475d 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
@@ -27,11 +27,6 @@ describe('Status Alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
status | title | variant | message | link
${DELETE_SCHEDULED} | ${SCHEDULED_FOR_DELETION_STATUS_TITLE} | ${'info'} | ${SCHEDULED_FOR_DELETION_STATUS_MESSAGE} | ${PACKAGE_DELETE_HELP_PAGE_PATH}
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 fa0d76762df..bfefe46c09b 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
@@ -50,16 +50,11 @@ describe('tags list row', () => {
},
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('checkbox', () => {
it('exists', () => {
mountComponent();
@@ -283,26 +278,30 @@ describe('tags list row', () => {
textSrOnly: true,
category: 'tertiary',
right: true,
+ disabled: false,
});
});
- it.each`
- canDelete | digest | disabled | buttonDisabled
- ${true} | ${null} | ${true} | ${true}
- ${false} | ${'foo'} | ${true} | ${true}
- ${false} | ${null} | ${true} | ${true}
- ${true} | ${'foo'} | ${true} | ${true}
- ${true} | ${'foo'} | ${false} | ${false}
- `(
- 'is $visible that is visible when canDelete is $canDelete and digest is $digest and disabled is $disabled',
- ({ canDelete, digest, disabled, buttonDisabled }) => {
- mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled });
+ it('has the correct classes', () => {
+ mountComponent();
- expect(findAdditionalActionsMenu().props('disabled')).toBe(buttonDisabled);
- expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(buttonDisabled);
- expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(buttonDisabled);
- },
- );
+ expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(false);
+ expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(false);
+ });
+
+ it('is not rendered when tag.canDelete is false', () => {
+ mountComponent({ ...defaultProps, tag: { ...tag, canDelete: false } });
+
+ expect(findAdditionalActionsMenu().exists()).toBe(false);
+ });
+
+ it('is hidden when disabled prop is set to true', () => {
+ mountComponent({ ...defaultProps, disabled: true });
+
+ expect(findAdditionalActionsMenu().props('disabled')).toBe(true);
+ expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(true);
+ expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(true);
+ });
describe('delete button', () => {
it('exists and has the correct attrs', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 1017ff06a25..09d0370efbf 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -68,15 +68,11 @@ describe('Tags List', () => {
resolver = jest.fn().mockResolvedValue(imageTagsMock());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('registry list', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent();
fireFirstSortUpdate();
- return waitForApolloRequestRender();
+ await waitForApolloRequestRender();
});
it('has a persisted search', () => {
@@ -98,6 +94,7 @@ describe('Tags List', () => {
pagination: tagsPageInfo,
items: tags,
idProperty: 'name',
+ hiddenDelete: false,
});
});
@@ -186,12 +183,23 @@ describe('Tags List', () => {
});
});
+ describe('when user does not have permission to delete list rows', () => {
+ it('sets registry list hiddenDelete prop to true', async () => {
+ resolver = jest.fn().mockResolvedValue(imageTagsMock({ canDelete: false }));
+ mountComponent();
+ fireFirstSortUpdate();
+ await waitForApolloRequestRender();
+
+ expect(findRegistryList().props('hiddenDelete')).toBe(true);
+ });
+ });
+
describe('when the list of tags is empty', () => {
- beforeEach(() => {
- resolver = jest.fn().mockResolvedValue(imageTagsMock([]));
+ beforeEach(async () => {
+ resolver = jest.fn().mockResolvedValue(imageTagsMock({ nodes: [] }));
mountComponent();
fireFirstSortUpdate();
- return waitForApolloRequestRender();
+ await waitForApolloRequestRender();
});
it('does not show the loader', () => {
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 88e79c513bc..8896185ce67 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
@@ -20,11 +20,6 @@ describe('TagsLoader component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
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 535faebdd4e..0d1d2c53cab 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
@@ -36,10 +36,6 @@ describe('cleanup_status', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
status | visible | text
${UNFINISHED_STATUS} | ${true} | ${CLEANUP_STATUS_UNFINISHED}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
index d2086943e4f..900ea61e4ea 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
@@ -26,10 +26,6 @@ describe('Registry Group Empty state', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
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 75068591007..7da9c7533a0 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
@@ -1,4 +1,4 @@
-import { GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
+import { GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective } from 'helpers/vue_mock_directive';
import { mockTracking } from 'helpers/tracking_helper';
@@ -49,16 +49,11 @@ describe('Image List Row', () => {
config: {},
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('image title and path', () => {
it('renders shortened name of image and contains a link to the details page', () => {
mountComponent();
@@ -206,13 +201,6 @@ describe('Image List Row', () => {
expect(findTagsCount().exists()).toBe(true);
});
- it('contains a tag icon', () => {
- mountComponent();
- const icon = findTagsCount().findComponent(GlIcon);
- expect(icon.exists()).toBe(true);
- expect(icon.props('name')).toBe('tag');
- });
-
describe('loading state', () => {
it('shows a loader when metadataLoading is true', () => {
mountComponent({ metadataLoading: true });
@@ -231,12 +219,12 @@ describe('Image List Row', () => {
it('with one tag in the image', () => {
mountComponent({ item: { ...item, tagsCount: 1 } });
- expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
+ expect(findTagsCount().text()).toMatchInterpolatedText('1 tag');
});
it('with more than one tag in the image', () => {
mountComponent({ item: { ...item, tagsCount: 3 } });
- expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
+ expect(findTagsCount().text()).toMatchInterpolatedText('3 tags');
});
});
});
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 042b8383571..6c771887b88 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
@@ -21,11 +21,6 @@ describe('Image List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('list', () => {
it('contains one list element for each image', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
index 8cfa8128021..e4d13143484 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
@@ -34,10 +34,6 @@ describe('Registry Project Empty state', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
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 bcc8e41fce8..45304cc2329 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
@@ -35,11 +35,6 @@ describe('registry_header', () => {
await nextTick();
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('header', () => {
it('has a title', () => {
mountComponent({ metadataLoading: true });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index e5b99f15e8c..cd54b856c97 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -177,11 +177,12 @@ export const tagsMock = [
},
];
-export const imageTagsMock = (nodes = tagsMock) => ({
+export const imageTagsMock = ({ nodes = tagsMock, canDelete = true } = {}) => ({
data: {
containerRepository: {
id: containerRepositoryMock.id,
tagsCount: nodes.length,
+ canDelete,
tags: {
nodes,
pageInfo: { ...tagsPageInfo },
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 26f0e506829..888c3e5bffa 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
@@ -127,11 +127,6 @@ describe('Details Page', () => {
jest.spyOn(Tracking, 'event');
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when isLoading is true', () => {
it('shows the loader', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index 1e514d85e82..acc61157ab5 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -113,10 +113,6 @@ describe('List Page', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains registry header', async () => {
mountComponent();
fireFirstSortUpdate();
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index 601f8abd34d..c2ae34ce697 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -31,11 +31,6 @@ import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock
const dummyApiVersion = 'v3000';
const dummyGrouptId = 1;
const dummyUrlRoot = '/gitlab';
-const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
-};
-let originalGon;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${dummyGrouptId}/dependency_proxy/cache`;
Vue.use(VueApollo);
@@ -89,16 +84,16 @@ describe('DependencyProxyApp', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
mock = new MockAdapter(axios);
mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {});
});
afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
mock.restore();
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
index 2f415bfd6f9..639a4fbb99d 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -25,10 +25,6 @@ describe('Manifests List', () => {
const findRows = () => wrapper.findAllComponents(ManifestRow);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has the correct title', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
index be3236d1f9c..ace5ce3a58d 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
@@ -29,10 +29,6 @@ describe('Manifest Row', () => {
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
const findStatus = () => wrapper.findByTestId('status');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('With a manifest on the DEFAULT status', () => {
beforeEach(() => {
createComponent();
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
index a2e5cbdce8b..1e9b9b1ce47 100644
--- 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
@@ -63,10 +63,6 @@ describe('Harbor artifact list row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('list item', () => {
beforeEach(() => {
mountComponent({
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
index b9d6dc2679e..786a4715731 100644
--- 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
@@ -26,10 +26,6 @@ describe('Harbor artifacts list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({
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
index e8cc2b2e22d..d8fb91c085c 100644
--- 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
@@ -20,10 +20,6 @@ describe('Harbor Details Header', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('artifact name', () => {
describe('missing image name', () => {
beforeEach(() => {
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 7a6169d300c..9a7ad759dba 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
@@ -29,10 +29,6 @@ describe('harbor_list_header', () => {
await nextTick();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('header', () => {
it('has a title', () => {
mountComponent({ metadataLoading: true });
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 b62d4e8836b..1e031e0557a 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
@@ -28,10 +28,6 @@ describe('Harbor List Row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
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 e7e74a0da58..a1803ecf7fb 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
@@ -20,10 +20,6 @@ describe('Harbor List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('list', () => {
it('contains one list element for each image', () => {
mountComponent();
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
index 5e299a269e3..9370ff1fdd4 100644
--- 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
@@ -26,10 +26,6 @@ describe('Harbor Tags Header', () => {
totalPages: 1,
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
mountComponent({
propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false },
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
index 849215e286b..0b2ce01ebf6 100644
--- 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
@@ -37,10 +37,6 @@ describe('Harbor tag list row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('list item', () => {
beforeEach(() => {
mountComponent({
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
index 4c6b2b6daaa..e2a2a584b7d 100644
--- 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
@@ -24,10 +24,6 @@ describe('Harbor Tags List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({
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
index 69765d31674..90c3d9082f7 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
@@ -74,10 +74,6 @@ describe('Harbor Details Page', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when isLoading is true', () => {
it('shows the loader', () => {
mountComponent();
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 97d30e6fe99..63ea8feb1e7 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
@@ -60,10 +60,6 @@ describe('Harbor List Page', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains harbor registry header', async () => {
mountComponent();
fireFirstSortUpdate();
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
index 10901c6ec1e..6002faa1fa3 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
@@ -60,10 +60,6 @@ describe('Harbor Tags page', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains tags header', () => {
mountComponent();
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 e74375b7705..f8130287c12 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
@@ -86,10 +86,6 @@ describe('PackagesApp', () => {
const findTerraformInstallation = () => wrapper.findComponent(TerraformInstallation);
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the app and displays the package title', async () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
index b504f7489ab..148e87699f1 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
@@ -39,10 +39,6 @@ describe('PackageTitle', () => {
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('module title', () => {
it('is correctly bound', async () => {
await createComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
index d7caa8ca2d8..7352afff051 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
@@ -23,10 +23,6 @@ describe('FileSha', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createComponent();
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 b76d7c2b57b..c3e0818fc11 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
@@ -37,11 +37,6 @@ describe('Package Files', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('rows', () => {
it('renders a single file for an npm package', () => {
createComponent();
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 0cbe2755f7e..a650aba464e 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
@@ -30,11 +30,6 @@ describe('Package History', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findHistoryElement = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findElementLink = (container) => container.findComponent(GlLink);
const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
index 78c1b840dbc..94797f01d16 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
@@ -30,10 +30,6 @@ describe('TerraformInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
index bb970336b94..ea4d268d84e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages_and_registries/infrastructure_registry/details/constants';
import {
fetchPackageVersions,
@@ -15,7 +15,7 @@ import {
} from '~/packages_and_registries/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/api.js');
describe('Actions Package details store', () => {
@@ -53,7 +53,7 @@ describe('Actions Package details store', () => {
expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.projectPackage = jest.fn().mockRejectedValue();
await testAction(
@@ -83,7 +83,7 @@ describe('Actions Package details store', () => {
packageEntity.id,
);
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
await testAction(deletePackage, undefined, { packageEntity }, [], []);
@@ -118,7 +118,7 @@ describe('Actions Package details store', () => {
});
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
await testAction(deletePackageFile, fileId, { packageEntity }, [], []);
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 801cde8582e..d0841c6110f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -49,9 +49,7 @@ exports[`packages_list_app renders 1`] = `
Learn how to
<b-link-stub
class="gl-link"
- event="click"
href="helpUrl"
- routertag="a"
target="_blank"
>
publish and share your packages
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
index a086c20a5e7..a89247c0a97 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
@@ -55,11 +55,6 @@ describe('Infrastructure Search', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('has a registry search component', () => {
mountComponent();
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 aca6b0942cc..7c7faa8a3b0 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
@@ -22,11 +22,6 @@ describe('Infrastructure Title', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('title area', () => {
beforeEach(() => {
mountComponent();
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 d237023d0cd..47d36d11e35 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
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
@@ -14,7 +14,7 @@ import InfrastructureSearch from '~/packages_and_registries/infrastructure_regis
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/lib/utils/common_utils');
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(Vuex);
@@ -72,10 +72,6 @@ describe('packages_list_app', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createStore({ packageCount: 1 });
mountComponent();
@@ -217,7 +213,7 @@ describe('packages_list_app', () => {
setWindowLocation(originalLocation);
});
- it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
+ it(`creates an alert if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
mountComponent();
expect(createAlert).toHaveBeenCalledWith({
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 0164d92ce34..51445942eaa 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
@@ -4,13 +4,13 @@ import Vue from 'vue';
import { last } from 'lodash';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import PackagesList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants';
import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
-import Tracking from '~/tracking';
import { packageList } from '../../mock_data';
Vue.use(Vuex);
@@ -72,11 +72,6 @@ describe('packages_list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when is loading', () => {
beforeEach(() => {
mountComponent({
@@ -179,23 +174,23 @@ describe('packages_list', () => {
});
describe('tracking', () => {
- let eventSpy;
+ let trackingSpy = null;
beforeEach(() => {
mountComponent();
- eventSpy = jest.spyOn(Tracking, 'event');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
- });
-
- it('deleteItemConfirmation calls event', () => {
- wrapper.vm.deleteItemConfirmation();
- expect(eventSpy).toHaveBeenCalledWith(
- TRACK_CATEGORY,
- TRACKING_ACTIONS.DELETE_PACKAGE,
- expect.any(Object),
- );
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('deleteItemConfirmation calls event', async () => {
+ await findPackageListDeleteModal().vm.$emit('ok');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACK_CATEGORY, TRACKING_ACTIONS.DELETE_PACKAGE, {
+ category: TRACK_CATEGORY,
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
index 2c185e040f4..4f051264172 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
@@ -2,14 +2,14 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants';
import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions';
import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/api.js');
describe('Actions Package list store', () => {
@@ -96,7 +96,7 @@ describe('Actions Package list store', () => {
});
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.projectPackages = jest.fn().mockRejectedValue();
await testAction(
actions.requestPackagesList,
@@ -198,7 +198,7 @@ describe('Actions Package list store', () => {
);
});
- it('should stop the loading and call create flash on api error', async () => {
+ it('should stop the loading and call create alert on api error', async () => {
mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.requestDeletePackage,
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
index 721bdd34a4f..37ca420ae77 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
@@ -49,16 +49,11 @@ describe('packages_list_row', () => {
disableDelete,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
index 9c1ebf5a2eb..d0817a8678e 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
@@ -45,7 +45,7 @@ describe('DeleteModal', () => {
it('passes actionPrimary prop', () => {
expect(findModal().props('actionPrimary')).toStrictEqual({
text: 'Permanently delete',
- attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ attributes: { variant: 'danger', category: 'primary' },
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
index 67f1906f6fd..9b429c39faa 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
@@ -89,8 +89,9 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
/>
<code-instruction-stub
+ class="gl-w-20 gl-mt-5"
copytext="Copy Maven command"
- instruction="mvn dependency:get -Dartifact=appGroup:appName:appVersion"
+ instruction="mvn install"
label="Maven Command"
trackingaction="copy_maven_command"
trackinglabel="code_instruction"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index b2375da7b11..5d390730ef1 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -20,7 +20,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<!---->
<button
aria-expanded="false"
- aria-haspopup="true"
+ aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
id="__BVID__27__BV_toggle_"
type="button"
@@ -59,7 +59,6 @@ exports[`PypiInstallation renders all the messages 1`] = `
</div>
<fieldset
- aria-describedby="installation-pip-command-group__BV_description_"
class="form-group gl-form-group"
id="installation-pip-command-group"
>
@@ -75,12 +74,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<!---->
</legend>
- <div
- aria-labelledby="installation-pip-command-group__BV_label_"
- class="bv-no-focus-ring"
- role="group"
- tabindex="-1"
- >
+ <div>
<div
data-testid="pip-command"
id="installation-pip-command"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
index 4f3d780b149..2e59c27cc1b 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
@@ -65,11 +65,6 @@ describe('Package Additional metadata', () => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTitle = () => wrapper.findByTestId('title');
const findMainArea = () => wrapper.findByTestId('main');
const findComponentIs = () => wrapper.findByTestId('component-is');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
index 0aba8f7efc7..a6298ebdea7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
@@ -34,10 +34,6 @@ describe('ComposerInstallation', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('install command switch', () => {
it('has the installation title component', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
index bf9425def9a..70534b1d0a6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
@@ -33,10 +33,6 @@ describe('ConanInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
index 9aed5b90c73..19aedf120b2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
@@ -19,10 +19,6 @@ describe('DependencyRow', () => {
const dependencyVersion = () => wrapper.findByTestId('version-pattern');
const dependencyFramework = () => wrapper.findByTestId('target-framework');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders', () => {
it('full dependency', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
index feed7a7c46c..a9428773a60 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
@@ -23,10 +23,6 @@ describe('FileSha', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js
index 5fe795f768e..a2d30be13c2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js
@@ -20,10 +20,6 @@ describe('InstallationTitle', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a title', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
index 8bb05b00e65..d35d95e319f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
@@ -40,10 +40,6 @@ describe('InstallationCommands', () => {
const pypiInstallation = () => wrapper.findComponent(PypiInstallation);
const composerInstallation = () => wrapper.findComponent(ComposerInstallation);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('installation instructions', () => {
describe.each`
packageEntity | selector
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
index fc60039db30..5ea81dccf7d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
@@ -35,7 +35,7 @@ describe('MavenInstallation', () => {
<artifactId>appName</artifactId>
<version>appVersion</version>
</dependency>`;
- const mavenCommandStr = 'mvn dependency:get -Dartifact=appGroup:appName:appVersion';
+ const mavenCommandStr = 'mvn install';
const mavenSetupXml = `<repositories>
<repository>
<id>gitlab-maven</id>
@@ -79,10 +79,6 @@ describe('MavenInstallation', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('install command switch', () => {
it('has the installation title component', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
index bb6846d354f..f2f3b8507c3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
@@ -18,11 +18,6 @@ describe('Composer Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findComposerTargetSha = () => wrapper.findByTestId('composer-target-sha');
const findComposerTargetShaCopyButton = () => wrapper.findComponent(ClipboardButton);
const findComposerJson = () => wrapper.findByTestId('composer-json');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
index e7e47401aa1..2832dc3a712 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
@@ -19,11 +19,6 @@ describe('Conan Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findConanRecipe = () => wrapper.findByTestId('conan-recipe');
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
index 8680d983042..7b253a26fc7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
@@ -19,11 +19,6 @@ describe('Maven Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findMavenApp = () => wrapper.findByTestId('maven-app');
const findMavenGroup = () => wrapper.findByTestId('maven-group');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
index af3692023f0..9fb467f9af1 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
@@ -19,11 +19,6 @@ describe('Nuget Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findNugetSource = () => wrapper.findByTestId('nuget-source');
const findNugetLicense = () => wrapper.findByTestId('nuget-license');
const findElementLink = (container) => container.findComponent(GlLink);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
index d7c6ea8379d..67f5fbc9e80 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
@@ -20,11 +20,6 @@ describe('Package Additional Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findPypiRequiredPython = () => wrapper.findByTestId('pypi-required-python');
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index 8c0e2d948ca..e711f9ee45d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -51,10 +51,6 @@ describe('NpmInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
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 9449c40c7c6..bcc0b78bfce 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
@@ -36,10 +36,6 @@ describe('NugetInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 529a6a22ddf..1dcac017ccf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -48,10 +48,6 @@ describe('Package Files', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rows', () => {
it('renders a single file for an npm package', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index bb2fa9eb6f5..ed470f63b8a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -63,11 +63,6 @@ describe('Package History', () => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader);
const findHistoryElement = (testId) => wrapper.findByTestId(testId);
const findElementLink = (container) => container.findComponent(GlLink);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index 1fda77f2aaa..ba21cdaca3b 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -38,7 +38,7 @@ describe('PackageTitle', () => {
},
provide,
directives: {
- GlResizeObserver: createMockDirective(),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
await nextTick();
@@ -55,10 +55,6 @@ describe('PackageTitle', () => {
const findSubHeaderText = () => wrapper.findByTestId('sub-header');
const findSubHeaderTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders', () => {
it('without tags', async () => {
await createComponent({ ...packageData(), packageFiles: { nodes: packageFiles() } });
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index 27c0ab96cfc..fc7f5c80d45 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,14 +1,18 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import Tracking from '~/tracking';
import {
+ CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ DELETE_PACKAGE_VERSION_TRACKING_ACTION,
DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import { packageData } from '../../mock_data';
@@ -22,7 +26,7 @@ describe('PackageVersionsList', () => {
name: 'version 1',
}),
packageData({
- id: `gid://gitlab/Packages::Package/112`,
+ id: 'gid://gitlab/Packages::Package/112',
name: 'version 2',
}),
];
@@ -31,8 +35,10 @@ describe('PackageVersionsList', () => {
findLoader: () => wrapper.findComponent(PackagesListLoader),
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
- findListRow: () => wrapper.findAllComponents(VersionRow),
+ findListRow: () => wrapper.findComponent(VersionRow),
+ findAllListRow: () => wrapper.findAllComponents(VersionRow),
findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
+ findPackageListDeleteModal: () => wrapper.findComponent(DeletePackageModal),
};
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackageVersionsList, {
@@ -118,16 +124,16 @@ describe('PackageVersionsList', () => {
});
it('displays package version rows', () => {
- expect(uiElements.findListRow().exists()).toEqual(true);
- expect(uiElements.findListRow()).toHaveLength(packageList.length);
+ expect(uiElements.findAllListRow().exists()).toEqual(true);
+ expect(uiElements.findAllListRow()).toHaveLength(packageList.length);
});
it('binds the correct props', () => {
- expect(uiElements.findListRow().at(0).props()).toMatchObject({
+ expect(uiElements.findAllListRow().at(0).props()).toMatchObject({
packageEntity: expect.objectContaining(packageList[0]),
});
- expect(uiElements.findListRow().at(1).props()).toMatchObject({
+ expect(uiElements.findAllListRow().at(1).props()).toMatchObject({
packageEntity: expect.objectContaining(packageList[1]),
});
});
@@ -159,6 +165,68 @@ describe('PackageVersionsList', () => {
});
});
+ describe.each`
+ description | finderFunction | deletePayload
+ ${'when the user can destroy the package'} | ${uiElements.findListRow} | ${packageList[0]}
+ ${'when the user can bulk destroy packages and deletes only one package'} | ${uiElements.findRegistryList} | ${[packageList[0]]}
+ `('$description', ({ finderFunction, deletePayload }) => {
+ let eventSpy;
+ const category = 'UI::NpmPackages';
+ const { findPackageListDeleteModal } = uiElements;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ mountComponent({ canDestroy: true });
+ finderFunction().vm.$emit('delete', deletePayload);
+ });
+
+ it('passes itemToBeDeleted to the modal', () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(packageList[0]);
+ });
+
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findPackageListDeleteModal().vm.$emit('ok');
+ });
+
+ it('emits delete when modal confirms', () => {
+ expect(wrapper.emitted('delete')[0][0]).toEqual([packageList[0]]);
+ });
+
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ DELETE_PACKAGE_VERSION_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
+ await findPackageListDeleteModal().vm.$emit(event);
+
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ });
+
+ it('canceling delete tracks the right action', () => {
+ findPackageListDeleteModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
describe('when the user can bulk destroy versions', () => {
let eventSpy;
const { findDeletePackagesModal, findRegistryList } = uiElements;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index 4a27f8011df..3f4358bb3b0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -29,10 +29,13 @@ password = <your personal access token>`;
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link');
- function createComponent() {
+ function createComponent(props = {}) {
wrapper = mountExtended(PypiInstallation, {
propsData: {
- packageEntity,
+ packageEntity: {
+ ...packageEntity,
+ ...props,
+ },
},
stubs: {
GlSprintf,
@@ -44,10 +47,6 @@ password = <your personal access token>`;
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('install command switch', () => {
it('has the installation title component', () => {
expect(findInstallationTitle().exists()).toBe(true);
@@ -86,6 +85,12 @@ password = <your personal access token>`;
});
});
+ it('does not have a link to personal access token docs when package is public', () => {
+ createComponent({ publicPackage: true });
+
+ expect(findAccessTokenLink().exists()).toBe(false);
+ });
+
it('has a link to the docs', () => {
expect(findSetupDocsLink().attributes()).toMatchObject({
href: PYPI_HELP_PATH,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
index 67340822fa5..f7c8e909ff6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
@@ -24,6 +24,7 @@ describe('VersionRow', () => {
const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
+ const findDeleteDropdownItem = () => wrapper.findComponent(GlDropdownItem);
function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, {
@@ -36,15 +37,11 @@ describe('VersionRow', () => {
GlTruncate,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a link to the version detail', () => {
createComponent();
@@ -112,6 +109,31 @@ describe('VersionRow', () => {
});
});
+ describe('delete button', () => {
+ it('does not exist when package cannot be destroyed', () => {
+ createComponent({ packageEntity: { ...packageVersion, canDestroy: false } });
+
+ expect(findDeleteDropdownItem().exists()).toBe(false);
+ });
+
+ it('exists and has the correct props', () => {
+ createComponent();
+
+ expect(findDeleteDropdownItem().exists()).toBe(true);
+ expect(findDeleteDropdownItem().attributes()).toMatchObject({
+ variant: 'danger',
+ });
+ });
+
+ it('emits the delete event when the delete button is clicked', () => {
+ createComponent();
+
+ findDeleteDropdownItem().vm.$emit('click');
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ });
+ });
+
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
index 689b53fa2a4..04546c4cea4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
@@ -14,7 +14,7 @@ import {
packagesListQuery,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('DeletePackages', () => {
let wrapper;
@@ -67,10 +67,6 @@ describe('DeletePackages', () => {
mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('binds deletePackages method to the default slot', () => {
createComponent();
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 2a78cfb13f9..91417d2fc9f 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
@@ -1,5 +1,5 @@
import { GlFormCheckbox, GlSprintf, GlTruncate } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -67,15 +67,11 @@ describe('packages_list_row', () => {
selected,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
@@ -141,7 +137,6 @@ describe('packages_list_row', () => {
findDeleteDropdown().vm.$emit('click');
- await nextTick();
expect(wrapper.emitted('delete')).toHaveLength(1);
});
});
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 610640e0ca3..ae990f3ea00 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
@@ -73,10 +73,6 @@ describe('packages_list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when is loading', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index a884959ab62..1250ecaf61f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -46,10 +46,6 @@ describe('Package Search', () => {
extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a registry search component', async () => {
mountComponent();
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 b47515e15c3..1296458155a 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
@@ -20,11 +20,6 @@ describe('PackageTitle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('title area', () => {
it('exists', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
index fcbd7cc6a50..e9119b736c2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
@@ -19,10 +19,6 @@ describe('publish_method', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
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 8f3c8667c47..c98f5f32344 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
@@ -19,11 +19,6 @@ describe('packages_filter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('binds all of his attrs to filtered search token', () => {
mountComponent({ attrs: { foo: 'bar' } });
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index d897be1f344..19c098e1f82 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -147,6 +147,7 @@ export const packageData = (extend) => ({
conanUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/conan',
pypiUrl:
'http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple',
+ publicPackage: false,
pypiSetupUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/pypi',
...extend,
});
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 b494965a3cb..49f69a46395 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
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
@@ -45,7 +45,7 @@ import {
pagination,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
useMockLocationHelper();
describe('PackagesApp', () => {
@@ -131,10 +131,6 @@ describe('PackagesApp', () => {
const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders an empty state component', async () => {
createComponent({ resolver: jest.fn().mockResolvedValue(emptyPackageDetailsQuery) });
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index a2ec527ce12..60bb055b1db 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -4,14 +4,13 @@ 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 { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import {
- PROJECT_RESOURCE_TYPE,
- GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
@@ -21,7 +20,7 @@ import getPackagesQuery from '~/packages_and_registries/package_registry/graphql
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
import { packagesListQuery, packageData, pagination } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('PackagesListApp', () => {
let wrapper;
@@ -78,10 +77,6 @@ describe('PackagesListApp', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const waitForFirstRequest = async () => {
// emit a search update so the query is executed
findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
@@ -171,14 +166,14 @@ describe('PackagesListApp', () => {
});
describe.each`
- type | sortType
- ${PROJECT_RESOURCE_TYPE} | ${'sort'}
- ${GROUP_RESOURCE_TYPE} | ${'groupSort'}
+ type | sortType
+ ${WORKSPACE_PROJECT} | ${'sort'}
+ ${WORKSPACE_GROUP} | ${'groupSort'}
`('$type query', ({ type, sortType }) => {
let provide;
let resolver;
- const isGroupPage = type === GROUP_RESOURCE_TYPE;
+ const isGroupPage = type === WORKSPACE_GROUP;
beforeEach(() => {
provide = { ...defaultProvide, isGroupPage };
@@ -198,9 +193,13 @@ describe('PackagesListApp', () => {
});
});
- describe('empty state', () => {
+ describe.each`
+ description | resolverResponse
+ ${'empty response'} | ${packagesListQuery({ extend: { nodes: [] } })}
+ ${'error response'} | ${{ data: { group: null } }}
+ `(`$description renders empty state`, ({ resolverResponse }) => {
beforeEach(() => {
- const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
+ const resolver = jest.fn().mockResolvedValue(resolverResponse);
mountComponent({ resolver });
return waitForFirstRequest();
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 796d89231f4..6dd4b9f2d20 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
@@ -28,7 +28,7 @@ import {
dependencyProxyUpdateTllPolicyMutationMock,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
describe('DependencyProxySettings', () => {
@@ -82,10 +82,6 @@ describe('DependencyProxySettings', () => {
.mockResolvedValue(dependencyProxyUpdateTllPolicyMutationMock());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle');
const findEnableTtlPoliciesToggle = () =>
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
index 86f14961690..461200e6983 100644
--- 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
@@ -23,10 +23,6 @@ describe('Exceptions Input', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
const findInput = () => wrapper.findComponent(GlFormInput);
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 7edc321867c..3ce8e91d43d 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -19,7 +19,7 @@ import {
dependencyProxyImageTtlPolicy,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Group Settings App', () => {
let wrapper;
@@ -55,10 +55,6 @@ describe('Group Settings App', () => {
show = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAlert = () => wrapper.findComponent(GlAlert);
const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings);
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 807f332f4d3..22e42f8c0ab 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
@@ -23,7 +23,7 @@ import {
groupPackageSettingsMutationErrorMock,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
describe('Packages Settings', () => {
@@ -56,10 +56,6 @@ describe('Packages Settings', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findDescription = () => wrapper.findByTestId('description');
const findMavenSettings = () => wrapper.findByTestId('maven-settings');
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
index a0b257a9496..d57077b31c8 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -25,7 +25,7 @@ import {
mavenProps,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
describe('Packages Forwarding Settings', () => {
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
index 2bb99fb8e8f..49e8601da88 100644
--- 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
@@ -61,10 +61,6 @@ describe('Cleanup image tags project settings', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
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 cbb5aa52694..57b48407174 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
@@ -124,10 +124,6 @@ describe('Container Expiration Policy Settings Form', () => {
jest.spyOn(Tracking, 'event');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
model | finder | fieldName | type | defaultValue
${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
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 43484d26d76..19f25d0aef7 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
@@ -63,10 +63,6 @@ describe('Container expiration policy project settings', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the setting form', async () => {
mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
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 ae41fdf65e0..058fe427106 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
@@ -32,11 +32,6 @@ describe('ExpirationDropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has a form-select component', () => {
mountComponent();
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 1cea0704154..be12d108d1e 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
@@ -38,11 +38,6 @@ describe('ExpirationInput', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has a label', () => {
mountComponent();
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 653f2a8b40e..f950a9d5add 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
@@ -23,11 +23,6 @@ describe('ExpirationToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has an input component', () => {
mountComponent();
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 55a66cebd83..ec7b89aa927 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
@@ -23,11 +23,6 @@ describe('ExpirationToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has a toggle component', () => {
mountComponent();
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 0fbbf4ae58f..b9c0c38bf9e 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
@@ -115,7 +115,6 @@ describe('Packages Cleanup Policy Settings Form', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
index 6dfeeca6862..94277d34f30 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
@@ -47,7 +47,6 @@ describe('Packages cleanup policy project settings', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
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 07d13839c61..54655acdf2a 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
@@ -20,11 +20,6 @@ describe('Registry Settings app', () => {
const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy);
const findAlert = () => wrapper.findComponent(GlAlert);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const defaultProvide = {
showContainerRegistrySettings: true,
showPackageRegistrySettings: true,
diff --git a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
index 18084766db9..41482e6e681 100644
--- a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
@@ -38,11 +38,6 @@ describe('cli_commands', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('shows the correct text on the button', () => {
expect(findDropdownButton().text()).toContain(QUICK_START);
});
diff --git a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
index 357dab593e8..ba5ba8f9884 100644
--- a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
@@ -19,11 +19,6 @@ describe('DeletePackageModal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when itemToBeDeleted prop is defined', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
index 93425d4f399..2490e9a1f6a 100644
--- a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
@@ -9,7 +9,7 @@ describe('PackagePath', () => {
wrapper = shallowMount(PackagePath, {
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -24,11 +24,6 @@ describe('PackagePath', () => {
const findItem = (name) => wrapper.find(`[data-testid="${name}"]`);
const findTooltip = (w) => getBinding(w.element, 'gl-tooltip');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
path | rootUrl | shouldExist | shouldNotExist
${'foo/bar'} | ${'/foo/bar'} | ${[]} | ${[ROOT_CHEVRON, ELLIPSIS_ICON, ELLIPSIS_CHEVRON, LEAF_LINK]}
diff --git a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
index 0005162e0bb..e43a9f57255 100644
--- a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
@@ -17,11 +17,6 @@ describe('PackagesListLoader', () => {
beforeEach(createComponent);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('desktop loader', () => {
it('produces the right loader', () => {
expect(findDesktopShapes().findAll('rect[width="1000"]')).toHaveLength(20);
diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
index db9f96bff39..1484377a475 100644
--- a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
@@ -43,10 +43,6 @@ describe('Persisted Search', () => {
extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a registry search component', async () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
index fa8f8f7641a..167599a54ea 100644
--- a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
@@ -20,11 +20,6 @@ describe('publish_method', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders', () => {
mountComponent(packageWithPipeline);
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
index 15db454ac68..c1f1a25d53b 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
@@ -31,10 +31,6 @@ describe('Registry Breadcrumb', () => {
nameGenerator.mockClear();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when is rootRoute', () => {
beforeEach(() => {
mountComponent(routes[0]);
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
index 2e2d5e26d33..a4e0d267023 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
@@ -36,10 +36,6 @@ describe('Registry List', () => {
const findScopedSlotFirstValue = (index) => findScopedSlots().at(index).find('span');
const findScopedSlotIsSelectedValue = (index) => findScopedSlots().at(index).find('p');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('header', () => {
it('renders the title passed in the prop', () => {
mountComponent();
@@ -111,10 +107,21 @@ describe('Registry List', () => {
expect(findDeleteSelected().text()).toBe(component.i18n.deleteSelected);
});
- it('is hidden when hiddenDelete is true', () => {
- mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } });
+ describe('when hiddenDelete is true', () => {
+ beforeEach(() => {
+ mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } });
+ });
- expect(findDeleteSelected().exists()).toBe(false);
+ it('is hidden', () => {
+ expect(findDeleteSelected().exists()).toBe(false);
+ });
+
+ it('populates the first slot prop correctly', async () => {
+ expect(findScopedSlots().at(0).exists()).toBe(true);
+
+ // it's the first slot
+ expect(findScopedSlotFirstValue(0).text()).toBe('false');
+ });
});
it('is disabled when isLoading is true', () => {
diff --git a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
index a4c1b989dac..664a821c275 100644
--- a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
@@ -15,10 +15,6 @@ describe('SettingsBlock', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDefaultSlot = () => wrapper.findByTestId('default-slot');
const findTitleSlot = () => wrapper.findByTestId('title-slot');
const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
index ec6369e7119..de6d44eabdc 100644
--- a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
@@ -19,8 +19,8 @@ describe('CancelJobs component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(CancelJobs, {
directives: {
- GlModal: createMockDirective(),
- GlTooltip: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js
index ab483316086..7ccded9b0f1 100644
--- a/spec/frontend/pages/groups/new/components/app_spec.js
+++ b/spec/frontend/pages/groups/new/components/app_spec.js
@@ -6,7 +6,7 @@ describe('App component', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(App, { propsData });
+ wrapper = shallowMount(App, { propsData: { groupsUrl: '/dashboard/groups', ...propsData } });
};
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
@@ -16,24 +16,31 @@ describe('App component', () => {
.props('panels')
.find((panel) => panel.name === 'create-group-pane');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('creates correct component for group creation', () => {
createComponent();
- expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('New group');
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/dashboard/groups', text: 'Groups' },
+ { href: '#', text: 'New group' },
+ ]);
expect(findCreateGroupPanel().title).toBe('Create group');
});
it('creates correct component for subgroup creation', () => {
- const props = { parentGroupName: 'parent', importExistingGroupPath: '/path' };
+ const detailProps = {
+ parentGroupName: 'parent',
+ importExistingGroupPath: '/path',
+ };
+
+ const props = { ...detailProps, parentGroupUrl: '/parent' };
createComponent(props);
- expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('parent');
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/parent', text: 'parent' },
+ { href: '#', text: 'New subgroup' },
+ ]);
expect(findCreateGroupPanel().title).toBe('Create subgroup');
- expect(findCreateGroupPanel().detailProps).toEqual(props);
+ expect(findCreateGroupPanel().detailProps).toEqual(detailProps);
});
});
diff --git a/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
index 56a1fd03f71..35015d84085 100644
--- a/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
+++ b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
@@ -15,10 +15,6 @@ describe('CreateGroupDescriptionDetails component', () => {
const findLinkHref = (at) => wrapper.findAllComponents(GlLink).at(at);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('creates correct component for group creation', () => {
createComponent();
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 da3954b4918..477511cde64 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
@@ -68,15 +68,10 @@ describe('BulkImportsHistoryApp', () => {
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const originalApiVersion = gon.api_version;
- beforeAll(() => {
+ beforeEach(() => {
gon.api_version = 'v4';
});
- afterAll(() => {
- gon.api_version = originalApiVersion;
- });
-
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
@@ -84,7 +79,6 @@ describe('BulkImportsHistoryApp', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('general behavior', () => {
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 628ee8d7999..239826c1458 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
@@ -21,22 +21,13 @@ describe('ImportErrorDetails', () => {
});
}
- const originalApiVersion = gon.api_version;
- beforeAll(() => {
- gon.api_version = 'v4';
- });
-
- afterAll(() => {
- gon.api_version = originalApiVersion;
- });
-
beforeEach(() => {
+ gon.api_version = 'v4';
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('general behavior', () => {
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 7d79583be19..43cbac25fe8 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
@@ -59,23 +59,14 @@ describe('ImportHistoryApp', () => {
});
}
- const originalApiVersion = gon.api_version;
- beforeAll(() => {
+ beforeEach(() => {
gon.api_version = 'v4';
gon.features = { fullPathProjectSearch: true };
- });
-
- afterAll(() => {
- gon.api_version = originalApiVersion;
- });
-
- beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('general behavior', () => {
diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
index c30b996437d..18a0098a715 100644
--- a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
+++ b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
@@ -25,8 +25,7 @@ describe('Password prompt modal', () => {
const findField = () => wrapper.findByTestId('password-prompt-field');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmBtn = () => findModal().props('actionPrimary');
- const findConfirmBtnDisabledState = () =>
- findModal().props('actionPrimary').attributes[2].disabled;
+ const findConfirmBtnDisabledState = () => findModal().props('actionPrimary').attributes.disabled;
const findCancelBtn = () => findModal().props('actionCancel');
@@ -41,10 +40,6 @@ describe('Password prompt modal', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the password field', () => {
expect(findField().exists()).toBe(true);
});
diff --git a/spec/frontend/pages/projects/forks/new/components/app_spec.js b/spec/frontend/pages/projects/forks/new/components/app_spec.js
index 0342b94a44d..e9a94878867 100644
--- a/spec/frontend/pages/projects/forks/new/components/app_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/app_spec.js
@@ -22,10 +22,6 @@ describe('App component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the correct svg illustration', () => {
expect(wrapper.find('img').attributes('src')).toBe('illustrations/project-create-new-sm.svg');
});
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 f0593a854b2..9dce6fde6f6 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
@@ -6,7 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
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';
@@ -14,7 +14,7 @@ import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_name
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('ForkForm component', () => {
@@ -111,7 +111,6 @@ describe('ForkForm component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -553,7 +552,7 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
});
- it('display flash when POST is unsuccessful', async () => {
+ it('displays an alert when POST is unsuccessful', async () => {
const dummyError = 'Fork project failed';
jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
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
index 82f451ed6ef..af578b69a81 100644
--- a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
@@ -3,15 +3,14 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
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('~/alert');
describe('ProjectNamespace component', () => {
let wrapper;
- let originalGon;
const data = {
project: {
@@ -85,14 +84,8 @@ describe('ProjectNamespace component', () => {
findListBox().vm.$emit('shown');
};
- beforeAll(() => {
- originalGon = window.gon;
- window.gon = { gitlab_url: gitlabUrl };
- });
-
- afterAll(() => {
- window.gon = originalGon;
- wrapper.destroy();
+ beforeEach(() => {
+ gon.gitlab_url = gitlabUrl;
});
describe('Initial state', () => {
@@ -152,7 +145,7 @@ describe('ProjectNamespace component', () => {
await nextTick();
});
- it('creates a flash message and captures the error', () => {
+ it('creates an alert message and captures the error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while loading data. Please refresh the page to try again.',
captureError: true,
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 5356953060a..882730d90ae 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlListbox, GlListboxItem } from '@gitlab/ui';
+import { GlAlert, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -22,7 +22,7 @@ describe('Code Coverage', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAreaChart = () => wrapper.findComponent(GlAreaChart);
- const findListBox = () => wrapper.findComponent(GlListbox);
+ const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListBoxItems = () => wrapper.findAllComponents(GlListboxItem);
const findFirstListBoxItem = () => findListBoxItems().at(0);
const findSecondListBoxItem = () => findListBoxItems().at(1);
@@ -37,15 +37,10 @@ describe('Code Coverage', () => {
graphRef,
graphCsvPath,
},
- stubs: { GlListbox },
+ stubs: { GlCollapsibleListbox },
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when fetching data is successful', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 2d3b9afa8f6..07d05293a3c 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -37,10 +37,6 @@ describe('Interval Pattern Input Component', () => {
const selectCustomRadio = () => findCustomRadio().setChecked(true);
const createWrapper = (props = {}, data = {}) => {
- if (wrapper) {
- throw new Error('A wrapper already exists');
- }
-
wrapper = mount(IntervalPatternInput, {
propsData: { ...props },
data() {
@@ -64,8 +60,6 @@ describe('Interval Pattern Input Component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
window.gl = oldWindowGl;
});
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 a633332ab65..e20c2fa47a7 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
@@ -31,10 +31,6 @@ describe('Pipeline Schedule Callout', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render the callout', () => {
expect(findInnerContentOfCallout().exists()).toBe(false);
});
@@ -46,10 +42,6 @@ describe('Pipeline Schedule Callout', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the callout container', () => {
expect(findInnerContentOfCallout().exists()).toBe(true);
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
index 5771e1b88e8..03c65ab4c9c 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -31,11 +31,6 @@ describe('Project Feature Settings', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Hidden name input', () => {
it('should set the hidden name input if the name exists', () => {
wrapper = mountComponent();
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
index 6230809a6aa..91d3057aec5 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
@@ -15,10 +15,6 @@ describe('Project Setting Row', () => {
wrapper = mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should show the label if it is set', async () => {
wrapper.setProps({ label: 'Test label' });
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 ff20b72c72c..0812be9745e 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
@@ -140,11 +140,6 @@ describe('Settings Panel', () => {
const findMonitorVisibilityInput = () =>
findMonitorSettings().findComponent(ProjectFeatureSetting);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Project Visibility', () => {
it('should set the project visibility help path', () => {
wrapper = mountComponent();
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js
index 6a18473b1a7..1858a56b0e1 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js
@@ -15,11 +15,6 @@ describe('WikiAlert', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLink = () => wrapper.findComponent(GlLink);
const findGlSprintf = () => wrapper.findComponent(GlSprintf);
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 c8e9a31b526..8e26453b564 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -31,11 +31,6 @@ describe('pages/shared/wikis/components/wiki_content', () => {
mock = new MockAdapter(axios);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findContent = () => wrapper.find('[data-testid="wiki-page-content"]');
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 ffcfd1d9f78..1be4a974f7a 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -103,8 +103,6 @@ describe('WikiForm', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
- wrapper = null;
});
it('displays markdown editor', () => {
@@ -116,7 +114,6 @@ describe('WikiForm', () => {
expect.objectContaining({
value: pageInfoPersisted.content,
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
- markdownDocsPath: pageInfoPersisted.markdownHelpPath,
uploadsPath: pageInfoPersisted.uploadsPath,
autofocus: pageInfoPersisted.persisted,
}),
@@ -126,6 +123,10 @@ describe('WikiForm', () => {
id: 'wiki_content',
name: 'wiki[content]',
});
+
+ expect(markdownEditor.vm.$attrs['markdown-docs-path']).toEqual(
+ pageInfoPersisted.markdownHelpPath,
+ );
});
it.each`
@@ -172,7 +173,7 @@ describe('WikiForm', () => {
nextTick();
- expect(findMarkdownEditor().props('enablePreview')).toBe(enabled);
+ expect(findMarkdownEditor().vm.$attrs['enable-preview']).toBe(enabled);
});
it.each`
diff --git a/spec/frontend/pdf/index_spec.js b/spec/frontend/pdf/index_spec.js
index 98946412264..23477c73ba0 100644
--- a/spec/frontend/pdf/index_spec.js
+++ b/spec/frontend/pdf/index_spec.js
@@ -7,10 +7,6 @@ describe('PDFLab component', () => {
const mountComponent = ({ pdf }) => shallowMount(PDFLab, { propsData: { pdf } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without PDF data', () => {
beforeEach(() => {
wrapper = mountComponent({ pdf: '' });
diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js
index 4cf83a3252d..1d5c5cd98c4 100644
--- a/spec/frontend/pdf/page_spec.js
+++ b/spec/frontend/pdf/page_spec.js
@@ -9,10 +9,6 @@ jest.mock('pdfjs-dist/webpack', () => {
describe('Page component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the page when mounting', async () => {
const testPage = {
render: jest.fn().mockReturnValue({ promise: Promise.resolve() }),
diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js
index 5460feb66fe..de9cc1e8008 100644
--- a/spec/frontend/performance_bar/components/add_request_spec.js
+++ b/spec/frontend/performance_bar/components/add_request_spec.js
@@ -13,10 +13,6 @@ describe('add request form', () => {
wrapper = mount(AddRequest);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('hides the input on load', () => {
expect(findGlFormInput().exists()).toBe(false);
});
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 5ab2c9abe5d..4194639fffe 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -38,10 +38,6 @@ describe('detailedMetric', () => {
const findAllSummaryItems = () =>
wrapper.findAllByTestId('performance-bar-summary-item').wrappers.map((w) => w.text());
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the current request has no details', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js
index 9dd8ea9f933..7b6d8ff695d 100644
--- a/spec/frontend/performance_bar/components/request_warning_spec.js
+++ b/spec/frontend/performance_bar/components/request_warning_spec.js
@@ -5,10 +5,6 @@ describe('request warning', () => {
let wrapper;
const htmlId = 'request-123';
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the request has warnings', () => {
beforeEach(() => {
wrapper = shallowMount(RequestWarning, {
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 6519989661f..376575a8acb 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PersistentUserCallout from '~/persistent_user_callout';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('PersistentUserCallout', () => {
const dismissEndpoint = '/dismiss';
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index fa30b9c2b97..8f44a6c085b 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -74,10 +74,6 @@ describe('Pipeline Wizard - Commit Page', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows a commit message input with the correct label', () => {
expect(wrapper.findByTestId('commit_message').exists()).toBe(true);
expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel);
@@ -121,10 +117,6 @@ describe('Pipeline Wizard - Commit Page', () => {
expect(wrapper.findByTestId('load-error').exists()).toBe(true);
expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError);
});
-
- afterEach(() => {
- wrapper.destroy();
- });
});
describe('commit result handling', () => {
@@ -151,7 +143,6 @@ describe('Pipeline Wizard - Commit Page', () => {
});
afterEach(() => {
- wrapper.destroy();
jest.clearAllMocks();
});
});
@@ -178,7 +169,6 @@ describe('Pipeline Wizard - Commit Page', () => {
});
afterEach(() => {
- wrapper.destroy();
jest.clearAllMocks();
});
});
@@ -246,10 +236,6 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets up without error', async () => {
expect(consoleSpy).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index dd0a609043a..6d7d4363189 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -9,10 +9,6 @@ describe('Pages Yaml Editor wrapper', () => {
propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mount hook', () => {
beforeEach(() => {
wrapper = mount(YamlEditor, defaultOptions);
diff --git a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
index f288264a11e..7f521e2523e 100644
--- a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
@@ -33,10 +33,6 @@ describe('Pipeline Wizard -- Input Wrapper', () => {
inputChild = wrapper.findComponent(TextWidget);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('will replace its value in compiled', async () => {
await inputChild.vm.$emit('input', inputValue);
const expected = new Document({
@@ -54,10 +50,6 @@ describe('Pipeline Wizard -- Input Wrapper', () => {
});
describe('Target Path Discovery', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
scenario | template | target | expected
${'simple nested object'} | ${{ foo: { bar: { baz: '$BOO' } } }} | ${'$BOO'} | ${['foo', 'bar', 'baz']}
diff --git a/spec/frontend/pipeline_wizard/components/step_nav_spec.js b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
index c6eac1386fa..8e2f0ab0281 100644
--- a/spec/frontend/pipeline_wizard/components/step_nav_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
@@ -19,10 +19,6 @@ describe('Pipeline Wizard - Step Navigation Component', () => {
nextButton = wrapper.findByTestId('next-button');
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
scenario | showBackButton | showNextButton
${'does not show prev button'} | ${false} | ${false}
diff --git a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
index b8e194015b0..52e5d49ec99 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
@@ -39,10 +39,6 @@ describe('Pipeline Wizard - Checklist Widget', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('creates the component', () => {
createComponent();
expect(wrapper.exists()).toBe(true);
diff --git a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
index c9e9f5caebe..b0eb7279a94 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
@@ -39,10 +39,6 @@ describe('Pipeline Wizard - List Widget', () => {
};
describe('component setup and interface', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('prints the label inside the legend', () => {
createComponent();
@@ -168,10 +164,6 @@ describe('Pipeline Wizard - List Widget', () => {
});
describe('form validation', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not show validation state when untouched', async () => {
createComponent({}, mountExtended);
expect(findGlFormGroup().classes()).not.toContain('is-valid');
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index 33c6394eb41..1056602c912 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -48,10 +48,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
wrapper.find(`[data-input-target="${target}"]`).find('input');
describe('display', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the steps', () => {
createComponent();
@@ -145,10 +141,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
}
});
- afterEach(() => {
- wrapper.destroy();
- });
-
if (expectCommitStepShown) {
it('does not show the step wrapper', async () => {
expect(wrapper.findComponent(WizardStep).isVisible()).toBe(false);
@@ -188,10 +180,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('initially shows a placeholder', async () => {
const editorContent = getEditorContent();
@@ -240,10 +228,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('highlight requests by the step get passed on to the editor', async () => {
const highlight = 'foo';
@@ -309,7 +293,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
afterEach(() => {
- wrapper.destroy();
inputField = undefined;
});
@@ -331,10 +314,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits done', () => {
expect(wrapper.emitted('done')).toBeUndefined();
diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
index 13234525159..e7bd7f686b6 100644
--- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
+++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
@@ -24,10 +24,6 @@ describe('PipelineWizard', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('mounts without error', () => {
const consoleSpy = jest.spyOn(console, 'error');
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
index 28a08b6da0f..aecaa640266 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
@@ -28,11 +28,6 @@ describe('The DAG annotations', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when there is one annotation', () => {
const currentNote = singleNote['dag-link103'];
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
index 4619548d1bb..6b46be3dd49 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -36,11 +36,6 @@ describe('The DAG graph', () => {
createComponent({ graphData: parsedData });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('in the basic case', () => {
beforeEach(() => {
/*
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index b0c26976c85..e2dc8120309 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -51,11 +51,6 @@ describe('Pipeline DAG graph wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when a query argument is undefined', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
index d1da7cb3acf..169e3666cbd 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue';
import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql';
@@ -12,7 +12,7 @@ import { mockFailedJobsQueryResponse, mockFailedJobsSummaryData } from '../../mo
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Failed Jobs App', () => {
let wrapper;
@@ -44,10 +44,6 @@ describe('Failed Jobs App', () => {
resolverSpy = jest.fn().mockResolvedValue(mockFailedJobsQueryResponse);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading spinner', () => {
it('displays loading spinner when fetching failed jobs', () => {
createComponent(resolverSpy);
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
index 0df15afd70d..0ac3b6c9074 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_table_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 { mountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql';
@@ -15,7 +15,7 @@ import {
mockPreparedFailedJobsDataNoPermission,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
@@ -45,10 +45,6 @@ describe('Failed Jobs Table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the failed jobs table', () => {
createComponent();
diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
index 9bc14266593..52df7b4500b 100644
--- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql';
@@ -12,7 +12,7 @@ import { mockPipelineJobsQueryResponse } from '../../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Jobs app', () => {
let wrapper;
@@ -45,10 +45,6 @@ describe('Jobs app', () => {
resolverSpy = jest.fn().mockResolvedValue(mockPipelineJobsQueryResponse);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading spinner', () => {
const setup = async () => {
createComponent(resolverSpy);
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
index 5ea57c51e70..a4ecb9041c9 100644
--- 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
@@ -19,7 +19,7 @@ describe('Linked pipeline mini list', () => {
const createComponent = (props = {}) => {
wrapper = mount(LinkedPipelinesMiniList, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
...props,
@@ -34,11 +34,6 @@ describe('Linked pipeline mini list', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render one linked pipeline item', () => {
expect(findLinkedPipelineMiniItem().exists()).toBe(true);
});
@@ -102,11 +97,6 @@ describe('Linked pipeline mini list', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render three linked pipeline items', () => {
expect(findLinkedPipelineMiniItems().exists()).toBe(true);
expect(findLinkedPipelineMiniItems().length).toBe(3);
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
index 036b82530d5..e7415a6c596 100644
--- 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
@@ -33,11 +33,6 @@ describe('Pipeline Mini Graph', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render the pipeline stages', () => {
expect(findPipelineStages().exists()).toBe(true);
});
@@ -71,11 +66,6 @@ describe('Pipeline Mini Graph', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should have the correct props', () => {
expect(findPipelineMiniGraph().props()).toMatchObject({
downstreamPipelines: [],
@@ -118,11 +108,6 @@ describe('Pipeline Mini Graph', () => {
});
});
- 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);
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index ab2056b4035..864f2d66f60 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -45,11 +45,10 @@ describe('Pipelines stage component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
eventHub.$emit.mockRestore();
mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
});
const findCiActionBtn = () => wrapper.find('.js-ci-action');
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index c123f53886e..73e810bde99 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -60,9 +60,4 @@ describe('Pipeline Stages', () => {
expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(true);
expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(true);
});
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
});
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index c2cb95d4320..337af6c1f60 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -39,10 +39,6 @@ describe('The Pipeline Tabs', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Tabs', () => {
it.each`
tabName | tabComponent
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index ba7262353f0..51a4487a3ef 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -51,8 +51,6 @@ describe('Pipelines filtered search', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
- wrapper = null;
});
it('displays UI elements', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
index 6531a15ab8e..b560eea4882 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
@@ -29,11 +29,6 @@ describe('CI Templates', () => {
const findTemplateName = () => wrapper.findByTestId('template-name');
const findTemplateLogo = () => wrapper.findByTestId('template-logo');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('renders template list', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
index 0c2938921d6..700be076e0c 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
@@ -37,11 +37,6 @@ describe('iOS Templates', () => {
const findSetupRunnerLink = () => wrapper.findByText('Set up a runner');
const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when ios runners are not available', () => {
beforeEach(() => {
wrapper = createWrapper({ iosRunnersAvailable: false });
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index f255e0d857f..0f4a2b1d02f 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -42,11 +42,6 @@ describe('Pipelines CI Templates', () => {
const findDocumentationLink = () => wrapper.findByTestId('documentation-link');
const findSettingsButton = () => wrapper.findByTestId('settings-button');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('renders test template', () => {
beforeEach(() => {
wrapper = createWrapper();
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 0abf7f59717..5465e4d77da 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -35,11 +35,6 @@ describe('Pipelines Empty State', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when user can configure CI', () => {
describe('when the ios_specific_templates experiment is active', () => {
beforeEach(() => {
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index e3eea503b46..890255f225e 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -33,7 +33,6 @@ describe('pipeline graph action component', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('render', () => {
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 99bccd21656..42e47a23db8 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -101,10 +101,6 @@ describe('Pipeline graph wrapper', () => {
createComponent({ apolloProvider, data, provide, mountFn });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when data is loading', () => {
it('displays the loading icon', () => {
createComponentWithApollo();
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 43587bebedf..78265165d1f 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -42,10 +42,6 @@ describe('the graph view selector component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when showing stage view', () => {
beforeEach(() => {
createComponent({ mountFn: mount });
diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
index d8afb33e148..1419a7b9982 100644
--- a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
+++ b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
@@ -69,10 +69,6 @@ describe('job group dropdown component', () => {
wrapper = mountFn(JobGroupDropdown, { propsData: { group } });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
createComponent({ mountFn: mount });
});
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 3224c87ab6b..5cc2c76f3dd 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,10 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { GlBadge, GlModal } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { GlBadge, GlModal, GlToast } from '@gitlab/ui';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
@@ -19,12 +20,14 @@ import {
describe('pipeline graph job item', () => {
useLocalStorageSpy();
+ Vue.use(GlToast);
let wrapper;
let mockAxios;
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
+ const findActionVueComponent = () => wrapper.findComponent(ActionComponent);
const findActionComponent = () => wrapper.findByTestId('ci-action-component');
const findBadge = () => wrapper.findComponent(GlBadge);
const findJobLink = () => wrapper.findByTestId('job-with-link');
@@ -41,9 +44,9 @@ describe('pipeline graph job item', () => {
job: mockJob,
};
- const createWrapper = ({ props, data } = {}) => {
+ const createWrapper = ({ props, data, mountFn = mount, mocks = {} } = {}) => {
wrapper = extendedWrapper(
- mount(JobItem, {
+ mountFn(JobItem, {
data() {
return {
...data,
@@ -53,6 +56,9 @@ describe('pipeline graph job item', () => {
...defaultProps,
...props,
},
+ mocks: {
+ ...mocks,
+ },
}),
);
};
@@ -238,6 +244,37 @@ describe('pipeline graph job item', () => {
});
});
+ describe('when retrying', () => {
+ const mockToastShow = jest.fn();
+
+ beforeEach(async () => {
+ createWrapper({
+ mountFn: shallowMount,
+ data: {
+ currentSkipModalValue: true,
+ },
+ props: {
+ skipRetryModal: true,
+ job: triggerJobWithRetryAction,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+
+ jest.spyOn(wrapper.vm.$toast, 'show');
+
+ await findActionVueComponent().vm.$emit('pipelineActionRequestComplete');
+ await nextTick();
+ });
+
+ it('shows a toast message that the downstream is being created', () => {
+ expect(mockToastShow).toHaveBeenCalledTimes(1);
+ });
+ });
+
describe('highlighting', () => {
it.each`
job | jobName | expanded | link
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index f396fe2aff4..b5ef10dee12 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -58,10 +58,6 @@ describe('Linked pipeline', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendered output', () => {
const props = {
pipeline: mockPipeline,
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 63e2d8707ea..6e4b9498918 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -65,10 +65,6 @@ describe('Linked Pipelines Column', () => {
createComponent({ apolloProvider, mountFn, props });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('it renders correctly', () => {
beforeEach(() => {
createComponentWithApollo();
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 19f597a7267..d4d7f1618c5 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -54,10 +54,6 @@ describe('stage column component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when mounted', () => {
beforeEach(() => {
createComponent({ method: mount });
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index 2c6d126e12c..50f754393fe 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -81,7 +81,6 @@ describe('Links Inner component', () => {
afterEach(() => {
jest.restoreAllMocks();
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index e2699d6ff2e..9d39c86ed5e 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -35,10 +35,6 @@ describe('links layer component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with show links off', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index e583c0798f5..a4d7d0e30f8 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -71,11 +71,6 @@ describe('Pipeline details header', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('initial loading', () => {
beforeEach(() => {
wrapper = createComponent(null, { isLoading: true });
diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js
index 2c4740df174..15de7dc51f1 100644
--- a/spec/frontend/pipelines/nav_controls_spec.js
+++ b/spec/frontend/pipelines/nav_controls_spec.js
@@ -1,23 +1,20 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
describe('Pipelines Nav Controls', () => {
let wrapper;
const createComponent = (props) => {
- wrapper = shallowMount(NavControls, {
+ wrapper = shallowMountExtended(NavControls, {
propsData: {
...props,
},
});
};
- const findRunPipeline = () => wrapper.find('.js-run-pipeline');
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
+ const findClearCacheButton = () => wrapper.findByTestId('clear-cache-button');
it('should render link to create a new pipeline', () => {
const mockData = {
@@ -28,9 +25,9 @@ describe('Pipelines Nav Controls', () => {
createComponent(mockData);
- const runPipeline = findRunPipeline();
- expect(runPipeline.text()).toContain('Run pipeline');
- expect(runPipeline.attributes('href')).toBe(mockData.newPipelinePath);
+ const runPipelineButton = findRunPipelineButton();
+ expect(runPipelineButton.text()).toContain('Run pipeline');
+ expect(runPipelineButton.attributes('href')).toBe(mockData.newPipelinePath);
});
it('should not render link to create pipeline if no path is provided', () => {
@@ -42,7 +39,7 @@ describe('Pipelines Nav Controls', () => {
createComponent(mockData);
- expect(findRunPipeline().exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
});
it('should render link for CI lint', () => {
@@ -54,9 +51,10 @@ describe('Pipelines Nav Controls', () => {
};
createComponent(mockData);
+ const ciLintButton = findCiLintButton();
- expect(wrapper.find('.js-ci-lint').text().trim()).toContain('CI lint');
- expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(mockData.ciLintPath);
+ expect(ciLintButton.text()).toContain('CI lint');
+ expect(ciLintButton.attributes('href')).toBe(mockData.ciLintPath);
});
describe('Reset Runners Cache', () => {
@@ -70,16 +68,13 @@ describe('Pipelines Nav Controls', () => {
});
it('should render button for resetting runner caches', () => {
- expect(wrapper.find('.js-clear-cache').text().trim()).toContain('Clear runner caches');
+ expect(findClearCacheButton().text()).toContain('Clear runner caches');
});
- it('should emit postAction event when reset runner cache button is clicked', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.find('.js-clear-cache').vm.$emit('click');
- await nextTick();
+ it('should emit postAction event when reset runner cache button is clicked', () => {
+ findClearCacheButton().vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo');
+ expect(wrapper.emitted('resetRunnersCache')).toEqual([['foo']]);
});
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index df10742fd93..123f2e011c3 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -39,10 +39,6 @@ describe('pipeline graph component', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with `VALID` status', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/pipelines/pipeline_labels_spec.js b/spec/frontend/pipelines/pipeline_labels_spec.js
index ca0229b1cbe..6a37e36352b 100644
--- a/spec/frontend/pipelines/pipeline_labels_spec.js
+++ b/spec/frontend/pipelines/pipeline_labels_spec.js
@@ -30,10 +30,6 @@ describe('Pipeline label component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not render tags when flags are not set', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index bedde71c48d..e3c9983aa52 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -67,8 +67,6 @@ describe('Pipeline Multi Actions Dropdown', () => {
afterEach(() => {
mockAxios.restore();
-
- wrapper.destroy();
});
it('should render the dropdown', () => {
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index 58bfb68e85c..856c0484075 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -22,15 +22,11 @@ describe('Pipelines Triggerer', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findTriggerer = () => wrapper.findByText('API');
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index c62898f0c83..f00ee4a6367 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -35,10 +35,6 @@ describe('Pipeline Url Component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render pipeline url table cell', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index e034d52a33c..2db9f5c2a83 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -5,7 +5,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -13,7 +13,7 @@ import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipeli
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Pipelines Actions dropdown', () => {
@@ -37,9 +37,6 @@ describe('Pipelines Actions dropdown', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
mock.restore();
confirmAction.mockReset();
});
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index e3e54716a7b..9fedbaf9b56 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -34,11 +34,6 @@ describe('Pipelines Artifacts dropdown', () => {
const findAllGlDropdownItems = () =>
wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render a dropdown with all the provided artifacts', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 2523b901506..48539d84024 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -11,7 +11,7 @@ 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';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
@@ -25,7 +25,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import { stageReply, users, mockSearch, branches } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';
@@ -114,7 +114,6 @@ describe('Pipelines', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.reset();
window.history.pushState.mockReset();
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 6ec8901038b..8d2a52eb6d0 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -69,12 +69,6 @@ describe('Pipelines Table', () => {
pipeline = createMockPipeline();
});
- afterEach(() => {
- wrapper.destroy();
-
- wrapper = null;
- });
-
describe('Pipelines Table', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' });
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index f6287107ed0..e05d2151f0a 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -2,13 +2,13 @@ import MockAdapter from 'axios-mock-adapter';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Actions TestReports Store', () => {
let mock;
@@ -49,7 +49,7 @@ describe('Actions TestReports Store', () => {
);
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
await testAction(
actions.fetchSummary,
null,
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index ed0cc71eb97..9c374ea817a 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -1,9 +1,9 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import mutations from '~/pipelines/stores/test_reports/mutations';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Mutations TestReports Store', () => {
let mockState;
@@ -58,7 +58,7 @@ describe('Mutations TestReports Store', () => {
expect(mockState.errorMessage).toBe(message);
});
- it('should show a flash message otherwise', () => {
+ it('should show an alert message otherwise', () => {
mutations[types.SET_SUITE_ERROR](mockState, {});
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index f194864447c..f8663408817 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -45,11 +45,6 @@ describe('Test case details', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('required details', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index 9b9ee4172f9..c8c917a1b9e 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -60,10 +60,6 @@ describe('Test reports app', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when component is created', () => {
it('should call fetchSummary when pipeline has test report', () => {
createComponent();
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 da13df833e7..8eb83f17f4d 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -65,10 +65,6 @@ describe('Test reports suite table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a message when there are no test cases', () => {
createComponent({ suite: [] });
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index f0da0df2ba6..efb1bf09d20 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -30,11 +30,6 @@ describe('Timeago component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const duration = () => wrapper.find('.duration');
const finishedAt = () => wrapper.find('.finished-at');
const findInProgress = () => wrapper.findByTestId('pipeline-in-progress');
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 caa66502e11..d518519a424 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -71,11 +71,6 @@ describe('Pipeline Branch Name Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(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 c090fd353f7..cf4ccb5ce43 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -45,11 +45,6 @@ describe('Pipeline Status Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(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 7311a5d2f5a..88c88d8f16f 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -53,11 +53,6 @@ describe('Pipeline Branch Name Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
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 c763bfe1b27..e9ec684a350 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -52,11 +52,6 @@ describe('Pipeline Trigger Author Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index 1299e7277d1..7f247fbbd4f 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -33,11 +33,6 @@ describe('popovers/components/popovers.vue', () => {
const allPopovers = () => wrapper.findAllComponents(GlPopover);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('addPopovers', () => {
it('attaches popovers to the targets specified', async () => {
const target = createPopoverTarget();
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 e4a316e1ee7..9a8f82f0028 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -40,12 +40,6 @@ describe('DeleteAccountModal component', () => {
vm = wrapper.vm;
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- vm = null;
- });
-
const findElements = () => {
const confirmation = vm.confirmWithPassword ? 'password' : 'username';
return {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index fa0e86a7b05..d922820601e 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -1,14 +1,15 @@
import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
-import { createAlert } from '~/flash';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UpdateUsername from '~/profile/account/components/update_username.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('UpdateUsername component', () => {
const rootUrl = TEST_HOST;
@@ -21,8 +22,10 @@ describe('UpdateUsername component', () => {
let wrapper;
let axiosMock;
+ const findNewUsernameInput = () => wrapper.findByTestId('new-username-input');
+
const createComponent = (props = {}) => {
- wrapper = shallowMount(UpdateUsername, {
+ wrapper = shallowMountExtended(UpdateUsername, {
propsData: {
...defaultProps,
...props,
@@ -39,8 +42,8 @@ describe('UpdateUsername component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
+ Vue.config.errorHandler = null;
});
const findElements = () => {
@@ -56,6 +59,13 @@ describe('UpdateUsername component', () => {
};
};
+ const clickModalWithErrorResponse = () => {
+ Vue.config.errorHandler = jest.fn(); // silence thrown error
+ const { modal } = findElements();
+ modal.vm.$emit('primary');
+ return waitForPromises();
+ };
+
it('has a disabled button if the username was not changed', async () => {
const { openModalBtn } = findElements();
@@ -80,11 +90,7 @@ describe('UpdateUsername component', () => {
beforeEach(async () => {
createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ newUsername });
-
- await nextTick();
+ await findNewUsernameInput().setValue(newUsername);
});
it('confirmation modal contains proper header and body', async () => {
@@ -100,14 +106,15 @@ describe('UpdateUsername component', () => {
axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
- await wrapper.vm.onConfirm();
- await nextTick();
+ const { modal } = findElements();
+ modal.vm.$emit('primary');
+ await waitForPromises();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
});
it('sets the username after a successful update', async () => {
- const { input, openModalBtn } = findElements();
+ const { input, openModalBtn, modal } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input.attributes('disabled')).toBe('disabled');
@@ -117,8 +124,8 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_OK, { message: 'Username changed' }];
});
- await wrapper.vm.onConfirm();
- await nextTick();
+ modal.vm.$emit('primary');
+ await waitForPromises();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(true);
@@ -136,7 +143,8 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
- await expect(wrapper.vm.onConfirm()).rejects.toThrow();
+ await clickModalWithErrorResponse();
+
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(false);
@@ -147,7 +155,7 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
- await expect(wrapper.vm.onConfirm()).rejects.toThrow();
+ await clickModalWithErrorResponse();
expect(createAlert).toHaveBeenCalledWith({
message: 'Invalid username',
@@ -159,7 +167,7 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST];
});
- await expect(wrapper.vm.onConfirm()).rejects.toThrow();
+ await clickModalWithErrorResponse();
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while updating your username, please try again.',
diff --git a/spec/frontend/profile/components/activity_calendar_spec.js b/spec/frontend/profile/components/activity_calendar_spec.js
new file mode 100644
index 00000000000..fb9dc7b22f7
--- /dev/null
+++ b/spec/frontend/profile/components/activity_calendar_spec.js
@@ -0,0 +1,120 @@
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import * as GitLabUIUtils from '@gitlab/ui/dist/utils';
+
+import ActivityCalendar from '~/profile/components/activity_calendar.vue';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useFakeDate } from 'helpers/fake_date';
+import { userCalendarResponse } from '../mock_data';
+
+jest.mock('~/lib/utils/ajax_cache');
+jest.mock('@gitlab/ui/dist/utils');
+
+describe('ActivityCalendar', () => {
+ // Feb 21st, 2023
+ useFakeDate(2023, 1, 21);
+
+ let wrapper;
+
+ const defaultProvide = {
+ userCalendarPath: '/users/root/calendar.json',
+ utcOffset: '0',
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(ActivityCalendar, { provide: defaultProvide });
+ };
+
+ const mockSuccessfulApiRequest = () =>
+ AjaxCache.retrieve.mockResolvedValueOnce(userCalendarResponse);
+ const mockUnsuccessfulApiRequest = () => AjaxCache.retrieve.mockRejectedValueOnce();
+
+ const findCalendar = () => wrapper.findByTestId('contrib-calendar');
+
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ AjaxCache.retrieve.mockReturnValueOnce(new Promise(() => {}));
+ });
+
+ it('renders loading icon', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(() => {
+ mockSuccessfulApiRequest();
+ });
+
+ it('renders the calendar', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findCalendar().exists()).toBe(true);
+ expect(wrapper.findByText(ActivityCalendar.i18n.calendarHint).exists()).toBe(true);
+ });
+
+ describe('when window is resized', () => {
+ it('re-renders the calendar', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ mockSuccessfulApiRequest();
+ window.innerWidth = 1200;
+ window.dispatchEvent(new Event('resize'));
+
+ await waitForPromises();
+
+ expect(findCalendar().exists()).toBe(true);
+ expect(AjaxCache.retrieve).toHaveBeenCalledTimes(2);
+ });
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ beforeEach(() => {
+ mockUnsuccessfulApiRequest();
+ });
+
+ it('renders error', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
+ });
+
+ describe('when retry button is clicked', () => {
+ it('retries API request', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ mockSuccessfulApiRequest();
+
+ await wrapper.findByRole('button', { name: ActivityCalendar.i18n.retry }).trigger('click');
+
+ await waitForPromises();
+
+ expect(findCalendar().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when screen is extra small', () => {
+ beforeEach(() => {
+ GitLabUIUtils.GlBreakpointInstance.getBreakpointSize.mockReturnValueOnce('xs');
+ });
+
+ it('does not render the calendar', () => {
+ createComponent();
+
+ expect(findCalendar().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
index 4af428c4e0c..9cc5bdea9be 100644
--- a/spec/frontend/profile/components/followers_tab_spec.js
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlBadge, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import FollowersTab from '~/profile/components/followers_tab.vue';
@@ -8,12 +8,25 @@ describe('FollowersTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowersTab);
+ wrapper = shallowMountExtended(FollowersTab, {
+ provide: {
+ followers: 2,
+ },
+ });
};
- it('renders `GlTab` and sets `title` prop', () => {
+ it('renders `GlTab` and sets title', () => {
createComponent();
- expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers'));
+ expect(wrapper.findComponent(GlTab).element.textContent).toContain(
+ s__('UserProfile|Followers'),
+ );
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
+ expect(wrapper.findComponent(GlBadge).element.textContent).toBe('2');
});
});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
index 75123274ccb..c9d56360c3e 100644
--- a/spec/frontend/profile/components/following_tab_spec.js
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlBadge, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import FollowingTab from '~/profile/components/following_tab.vue';
@@ -8,12 +8,25 @@ describe('FollowingTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowingTab);
+ wrapper = shallowMountExtended(FollowingTab, {
+ provide: {
+ followees: 3,
+ },
+ });
};
- it('renders `GlTab` and sets `title` prop', () => {
+ it('renders `GlTab` and sets title', () => {
createComponent();
- expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following'));
+ expect(wrapper.findComponent(GlTab).element.textContent).toContain(
+ s__('UserProfile|Following'),
+ );
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
+ expect(wrapper.findComponent(GlBadge).element.textContent).toBe('3');
});
});
diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js
index eb27515bca3..d4cb1dfd15d 100644
--- a/spec/frontend/profile/components/overview_tab_spec.js
+++ b/spec/frontend/profile/components/overview_tab_spec.js
@@ -3,6 +3,7 @@ import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import OverviewTab from '~/profile/components/overview_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ActivityCalendar from '~/profile/components/activity_calendar.vue';
describe('OverviewTab', () => {
let wrapper;
@@ -16,4 +17,10 @@ describe('OverviewTab', () => {
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview'));
});
+
+ it('renders `ActivityCalendar` component', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(ActivityCalendar).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js
new file mode 100644
index 00000000000..5b584eff362
--- /dev/null
+++ b/spec/frontend/profile/components/user_achievements_spec.js
@@ -0,0 +1,102 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json';
+import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json';
+import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json';
+import getUserAchievementsNoAvatarResponse from 'test_fixtures/graphql/get_user_achievements_without_avatar_or_description_response.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import UserAchievements from '~/profile/components/user_achievements.vue';
+import getUserAchievements from '~/profile/components//graphql/get_user_achievements.query.graphql';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+const USER_ID = 123;
+const ROOT_URL = 'https://gitlab.com/';
+const PLACEHOLDER_URL = 'https://gitlab.com/assets/gitlab_logo.png';
+const userAchievement1 = getUserAchievementsResponse.data.user.userAchievements.nodes[0];
+
+Vue.use(VueApollo);
+
+describe('UserAchievements', () => {
+ let wrapper;
+
+ const getUserAchievementsQueryHandler = jest.fn().mockResolvedValue(getUserAchievementsResponse);
+ const achievement = () => wrapper.findByTestId('user-achievement');
+
+ const createComponent = ({ queryHandler = getUserAchievementsQueryHandler } = {}) => {
+ const fakeApollo = createMockApollo([[getUserAchievements, queryHandler]]);
+
+ wrapper = mountExtended(UserAchievements, {
+ apolloProvider: fakeApollo,
+ provide: {
+ rootUrl: ROOT_URL,
+ userId: USER_ID,
+ },
+ });
+ };
+
+ it('renders no achievements on reject', async () => {
+ createComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('user-achievement').length).toBe(0);
+ });
+
+ it('renders no achievements when none are present', async () => {
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsEmptyResponse),
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('user-achievement').length).toBe(0);
+ });
+
+ it('only renders 3 achievements when more are present', async () => {
+ createComponent({ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsLongResponse) });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('user-achievement').length).toBe(3);
+ });
+
+ it('renders achievement correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(achievement().text()).toContain(userAchievement1.achievement.name);
+ expect(achievement().text()).toContain(
+ `Awarded ${timeagoMixin.methods.timeFormatted(userAchievement1.createdAt)} by`,
+ );
+ expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
+ expect(achievement().text()).toContain(userAchievement1.achievement.description);
+ expect(achievement().find('img').attributes('src')).toBe(
+ userAchievement1.achievement.avatarUrl,
+ );
+ });
+
+ it('renders a placeholder when no avatar is present', async () => {
+ gon.gitlab_logo = PLACEHOLDER_URL;
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse),
+ });
+
+ await waitForPromises();
+
+ expect(achievement().find('img').attributes('src')).toBe(PLACEHOLDER_URL);
+ });
+
+ it('does not render a description when none is present', async () => {
+ gon.gitlab_logo = PLACEHOLDER_URL;
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse),
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('achievement-description').length).toBe(0);
+ });
+});
diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js
new file mode 100644
index 00000000000..7106ea84619
--- /dev/null
+++ b/spec/frontend/profile/mock_data.js
@@ -0,0 +1,22 @@
+export const userCalendarResponse = {
+ '2022-11-18': 13,
+ '2022-11-19': 21,
+ '2022-11-20': 14,
+ '2022-11-21': 15,
+ '2022-11-22': 20,
+ '2022-11-23': 21,
+ '2022-11-24': 15,
+ '2022-11-25': 14,
+ '2022-11-26': 16,
+ '2022-11-27': 13,
+ '2022-11-28': 4,
+ '2022-11-29': 1,
+ '2022-11-30': 1,
+ '2022-12-13': 1,
+ '2023-01-10': 3,
+ '2023-01-11': 2,
+ '2023-01-20': 1,
+ '2023-02-02': 1,
+ '2023-02-06': 2,
+ '2023-02-07': 2,
+};
diff --git a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
index e60602ab336..e69bfad765a 100644
--- a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
+++ b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
@@ -12,11 +12,6 @@ describe('DiffsColorsPreview component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders diff colors preview', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/profile/preferences/components/diffs_colors_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_spec.js
index 02f501a0b06..e80851d0629 100644
--- a/spec/frontend/profile/preferences/components/diffs_colors_spec.js
+++ b/spec/frontend/profile/preferences/components/diffs_colors_spec.js
@@ -29,11 +29,6 @@ describe('DiffsColors component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('mounts', () => {
createComponent();
diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js
index f650bee7fda..b809f2f4aed 100644
--- a/spec/frontend/profile/preferences/components/integration_view_spec.js
+++ b/spec/frontend/profile/preferences/components/integration_view_spec.js
@@ -38,11 +38,6 @@ describe('IntegrationView component', () => {
const findHiddenField = () =>
wrapper.findByTestId('profile-preferences-integration-hidden-field');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render the form group legend correctly', () => {
wrapper = createComponent();
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 91cd868daac..21167dccda9 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/alert';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
import { i18n } from '~/profile/preferences/constants';
@@ -17,7 +17,7 @@ import {
lightModeThemeId2,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const expectedUrl = '/foo';
useMockLocationHelper();
@@ -83,11 +83,6 @@ describe('ProfilePreferences component', () => {
document.body.classList.add('content-wrapper');
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should not render Integrations section', () => {
wrapper = createComponent();
const views = wrapper.findAllComponents(IntegrationView);
diff --git a/spec/frontend/profile/utils_spec.js b/spec/frontend/profile/utils_spec.js
new file mode 100644
index 00000000000..43537afe169
--- /dev/null
+++ b/spec/frontend/profile/utils_spec.js
@@ -0,0 +1,15 @@
+import { getVisibleCalendarPeriod } from '~/profile/utils';
+import { CALENDAR_PERIOD_12_MONTHS, CALENDAR_PERIOD_6_MONTHS } from '~/profile/constants';
+
+describe('getVisibleCalendarPeriod', () => {
+ it.each`
+ width | expected
+ ${1000} | ${CALENDAR_PERIOD_12_MONTHS}
+ ${900} | ${CALENDAR_PERIOD_6_MONTHS}
+ `('returns $expected when container width is $width', ({ width, expected }) => {
+ const container = document.createElement('div');
+ jest.spyOn(container, 'getBoundingClientRect').mockReturnValueOnce({ width });
+
+ expect(getVisibleCalendarPeriod(container)).toBe(expected);
+ });
+});
diff --git a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
index d230b96ad82..68ea3a4dc4d 100644
--- a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
+++ b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
@@ -26,10 +26,6 @@ describe('ClustersDeprecationAlert', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('should render a non-dismissible warning alert', () => {
expect(findAlert().props()).toMatchObject({
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 6aa5a9a5a3a..0e68bd21cd4 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -12,7 +12,7 @@ describe('BranchesDropdown', () => {
let store;
const spyFetchBranches = jest.fn();
- const createComponent = (props, state = { isFetching: false }) => {
+ const createComponent = (props, state = { isFetching: false, branch: '_main_' }) => {
store = new Vuex.Store({
getters: {
joinedBranches: () => ['_main_', '_branch_1_', '_branch_2_'],
@@ -41,8 +41,6 @@ describe('BranchesDropdown', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
spyFetchBranches.mockReset();
});
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index c59cf700e0d..84cb30953c3 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -55,7 +55,6 @@ describe('CommitFormModal', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -166,7 +165,7 @@ describe('CommitFormModal', () => {
it('Changes the target_project_id input value', async () => {
createComponent(shallowMount, {}, {}, { isCherryPick: true });
- findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_');
+ findProjectsDropdown().vm.$emit('input', '_changed_project_value_');
await nextTick();
diff --git a/spec/frontend/projects/commit/components/projects_dropdown_spec.js b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
index 0e213ff388a..baf2ea2656f 100644
--- a/spec/frontend/projects/commit/components/projects_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectsDropdown from '~/projects/commit/components/projects_dropdown.vue';
@@ -38,7 +38,6 @@ describe('ProjectsDropdown', () => {
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
afterEach(() => {
- wrapper.destroy();
spyFetchProjects.mockReset();
});
@@ -48,20 +47,24 @@ describe('ProjectsDropdown', () => {
});
describe('Custom events', () => {
- it('should emit selectProject if a project is clicked', () => {
+ it('should emit input if a project is clicked', () => {
findDropdown().vm.$emit('select', '1');
- expect(wrapper.emitted('selectProject')).toEqual([['1']]);
+ expect(wrapper.emitted('input')).toEqual([['1']]);
});
});
});
describe('Case insensitive for search term', () => {
beforeEach(() => {
- createComponent('_PrOjEcT_1_');
+ createComponent('_PrOjEcT_1_', { targetProjectId: '1' });
});
- it('renders only the project searched for', () => {
+ it('renders only the project searched for', async () => {
+ findDropdown().vm.$emit('search', '_project_1_');
+
+ await nextTick();
+
expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]);
});
});
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index 008710984b9..d48f9fd6fc0 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
import * as actions from '~/projects/commit/store/actions';
@@ -8,7 +8,7 @@ import * as types from '~/projects/commit/store/mutation_types';
import getInitialState from '~/projects/commit/store/state';
import mockData from '../mock_data';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Commit form modal store actions', () => {
let axiosMock;
@@ -63,7 +63,7 @@ describe('Commit form modal store actions', () => {
);
});
- it('should show flash error and set error in state on fetchBranches failure', async () => {
+ it('should show alert error and set error in state on fetchBranches failure', async () => {
jest.spyOn(axios, 'get').mockRejectedValue();
await testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }]);
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 907e0e226b6..ff1d860fd53 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -54,7 +54,6 @@ describe('Author Select', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index bae9c48fc1e..f5184e59420 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -1,13 +1,13 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Project commits actions', () => {
let state;
@@ -34,8 +34,8 @@ describe('Project commits actions', () => {
]));
});
- describe('shows a flash message when there is an error', () => {
- it('creates a flash', () => {
+ describe('shows an alert message when there is an error', () => {
+ it('creates an alert', () => {
const mockDispatchContext = { dispatch: () => {}, commit: () => {}, state };
actions.receiveAuthorsError(mockDispatchContext);
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index 9b052a17caa..ee96f46ea0c 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -21,11 +21,6 @@ describe('CompareApp component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
index 21cca857c6a..0b1085470b8 100644
--- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
@@ -16,11 +16,6 @@ describe('RepoDropdown component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js
index b23bd91ceda..3c9c61c8903 100644
--- a/spec/frontend/projects/compare/components/revision_card_spec.js
+++ b/spec/frontend/projects/compare/components/revision_card_spec.js
@@ -16,11 +16,6 @@ describe('RepoDropdown component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(() => {
createComponent();
});
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 53763bd7d8f..645d0483a5f 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -1,8 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue';
@@ -14,7 +14,7 @@ const defaultProps = {
paramsBranch: 'main',
};
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RevisionDropdown component', () => {
let wrapper;
@@ -35,11 +35,14 @@ describe('RevisionDropdown component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findBranchesDropdownItem = () =>
+ wrapper.findAllComponents('[data-testid="branches-dropdown-item"]');
+ const findTagsDropdownItem = () =>
+ wrapper.findAllComponents('[data-testid="tags-dropdown-item"]');
it('sets hidden input', () => {
expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe(
@@ -58,10 +61,21 @@ describe('RevisionDropdown component', () => {
createComponent();
- await axios.waitForAll();
+ expect(findBranchesDropdownItem()).toHaveLength(0);
+ expect(findTagsDropdownItem()).toHaveLength(0);
- expect(wrapper.vm.branches).toEqual(Branches);
- expect(wrapper.vm.tags).toEqual(Tags);
+ await waitForPromises();
+
+ Branches.forEach((branch, index) => {
+ expect(findBranchesDropdownItem().at(index).text()).toBe(branch);
+ });
+
+ Tags.forEach((tag, index) => {
+ expect(findTagsDropdownItem().at(index).text()).toBe(tag);
+ });
+
+ expect(findBranchesDropdownItem()).toHaveLength(Branches.length);
+ expect(findTagsDropdownItem()).toHaveLength(Tags.length);
});
it('sets branches and tags to be an empty array when no tags or branches are given', async () => {
@@ -70,16 +84,17 @@ describe('RevisionDropdown component', () => {
Tags: undefined,
});
- await axios.waitForAll();
+ await waitForPromises();
- expect(wrapper.vm.branches).toEqual([]);
- expect(wrapper.vm.tags).toEqual([]);
+ expect(findBranchesDropdownItem()).toHaveLength(0);
+ expect(findTagsDropdownItem()).toHaveLength(0);
});
- it('shows flash message on error', async () => {
+ it('shows alert message on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
- await wrapper.vm.fetchBranchesAndTags();
+ await waitForPromises();
+
expect(createAlert).toHaveBeenCalled();
});
@@ -102,17 +117,19 @@ describe('RevisionDropdown component', () => {
it('emits a "selectRevision" event when a revision is selected', async () => {
const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
+ const branchName = 'some-branch';
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ branches: ['some-branch'] });
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
+ Branches: [branchName],
+ });
- await nextTick();
+ createComponent();
+ await waitForPromises();
findFirstGlDropdownItem().vm.$emit('click');
expect(wrapper.emitted()).toEqual({
- selectRevision: [[{ direction: 'from', revision: 'some-branch' }]],
+ selectRevision: [[{ direction: 'from', revision: branchName }]],
});
});
});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index db4a1158996..3a256682549 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -2,13 +2,14 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
import { revisionDropdownDefaultProps as defaultProps } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RevisionDropdown component', () => {
let wrapper;
@@ -32,12 +33,15 @@ describe('RevisionDropdown component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findBranchesDropdownItem = () =>
+ wrapper.findAllComponents('[data-testid="branches-dropdown-item"]');
+ const findTagsDropdownItem = () =>
+ wrapper.findAllComponents('[data-testid="tags-dropdown-item"]');
it('sets hidden input', () => {
createComponent();
@@ -57,17 +61,29 @@ describe('RevisionDropdown component', () => {
createComponent();
- await axios.waitForAll();
- expect(wrapper.vm.branches).toEqual(Branches);
- expect(wrapper.vm.tags).toEqual(Tags);
+ expect(findBranchesDropdownItem()).toHaveLength(0);
+ expect(findTagsDropdownItem()).toHaveLength(0);
+
+ await waitForPromises();
+
+ expect(findBranchesDropdownItem()).toHaveLength(Branches.length);
+ expect(findTagsDropdownItem()).toHaveLength(Tags.length);
+
+ Branches.forEach((branch, index) => {
+ expect(findBranchesDropdownItem().at(index).text()).toBe(branch);
+ });
+
+ Tags.forEach((tag, index) => {
+ expect(findTagsDropdownItem().at(index).text()).toBe(tag);
+ });
});
- it('shows flash message on error', async () => {
+ it('shows alert message on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
+ await waitForPromises();
- await wrapper.vm.fetchBranchesAndTags();
expect(createAlert).toHaveBeenCalled();
});
@@ -83,17 +99,17 @@ describe('RevisionDropdown component', () => {
refsProjectPath: newRefsProjectPath,
});
- await axios.waitForAll();
+ await waitForPromises();
expect(axios.get).toHaveBeenLastCalledWith(newRefsProjectPath);
});
describe('search', () => {
- it('shows flash message on error', async () => {
+ it('shows alert message on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
+ await waitForPromises();
- await wrapper.vm.searchBranchesAndTags();
expect(createAlert).toHaveBeenCalled();
});
@@ -108,7 +124,7 @@ describe('RevisionDropdown component', () => {
const mockSearchTerm = 'foobar';
createComponent();
findSearchBox().vm.$emit('input', mockSearchTerm);
- await axios.waitForAll();
+ await waitForPromises();
expect(axios.get).toHaveBeenCalledWith(
defaultProps.refsProjectPath,
@@ -141,8 +157,14 @@ describe('RevisionDropdown component', () => {
});
it('emits `selectRevision` event when another revision is selected', async () => {
+ jest.spyOn(axios, 'get').mockResolvedValue({
+ data: {
+ Branches: ['some-branch'],
+ Tags: [],
+ },
+ });
+
createComponent();
- wrapper.vm.branches = ['some-branch'];
await nextTick();
findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click');
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
index 49e3218e5bc..bae76e7eeb6 100644
--- a/spec/frontend/projects/components/project_delete_button_spec.js
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -33,11 +33,6 @@ describe('Project remove modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('initialized', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
index 097b18025a3..364a29d0e41 100644
--- a/spec/frontend/projects/components/shared/delete_button_spec.js
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -45,11 +45,6 @@ describe('Project remove modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('intialized', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js
index 50638755260..e9b11ce544a 100644
--- a/spec/frontend/projects/details/upload_button_spec.js
+++ b/spec/frontend/projects/details/upload_button_spec.js
@@ -27,10 +27,6 @@ describe('UploadButton', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays an upload button', () => {
expect(wrapper.findComponent(GlButton).exists()).toBe(true);
});
diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index f6edbab3cca..5b2dc25077e 100644
--- a/spec/frontend/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -6,12 +6,12 @@ describe('Experimental new project creation app', () => {
let wrapper;
const createComponent = (propsData) => {
- wrapper = shallowMount(App, { propsData });
+ wrapper = shallowMount(App, {
+ propsData: { projectsUrl: '/dashboard/projects', ...propsData },
+ });
};
- afterEach(() => {
- wrapper.destroy();
- });
+ const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
it('passes custom new project guideline text to underlying component', () => {
const DEMO_GUIDELINES = 'Demo guidelines';
@@ -34,11 +34,28 @@ describe('Experimental new project creation app', () => {
expect(
Boolean(
- wrapper
- .findComponent(NewNamespacePage)
+ findNewNamespacePage()
.props()
.panels.find((p) => p.name === 'cicd_for_external_repo'),
),
).toBe(isCiCdAvailable);
});
+
+ it('creates correct breadcrumbs for top-level projects', () => {
+ createComponent();
+
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/dashboard/projects', text: 'Projects' },
+ { href: '#', text: 'New project' },
+ ]);
+ });
+
+ it('creates correct breadcrumbs for projects within groups', () => {
+ createComponent({ parentGroupUrl: '/parent-group', parentGroupName: 'Parent Group' });
+
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/parent-group', text: 'Parent Group' },
+ { href: '#', text: 'New project' },
+ ]);
+ });
});
diff --git a/spec/frontend/projects/new/components/deployment_target_select_spec.js b/spec/frontend/projects/new/components/deployment_target_select_spec.js
index f3b22d4a1b9..bec738f7765 100644
--- a/spec/frontend/projects/new/components/deployment_target_select_spec.js
+++ b/spec/frontend/projects/new/components/deployment_target_select_spec.js
@@ -47,7 +47,6 @@ describe('Deployment target select', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
index 16b4493c622..1a43dcb682b 100644
--- a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
+++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
@@ -37,7 +37,6 @@ describe('New project push tip popover', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index 67532cea61e..fa720f4487c 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -3,8 +3,8 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlSearchBoxByType,
GlTruncate,
+ GlSearchBoxByType,
} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -12,6 +12,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { stubComponent } from 'helpers/stub_component';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/projects/new/event_hub';
import NewProjectUrlSelect from '~/projects/new/components/new_project_url_select.vue';
@@ -68,6 +69,7 @@ describe('NewProjectUrlSelect component', () => {
};
let mockQueryResponse;
+ let focusInputSpy;
const mountComponent = ({
search = '',
@@ -78,6 +80,7 @@ describe('NewProjectUrlSelect component', () => {
mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse });
const requestHandlers = [[searchQuery, mockQueryResponse]];
const apolloProvider = createMockApollo(requestHandlers);
+ focusInputSpy = jest.fn();
return mountFn(NewProjectUrlSelect, {
apolloProvider,
@@ -87,13 +90,17 @@ describe('NewProjectUrlSelect component', () => {
search,
};
},
+ stubs: {
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputSpy },
+ }),
+ },
});
};
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findSelectedPath = () => wrapper.findComponent(GlTruncate);
- const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenNamespaceInput = () => wrapper.find(`[name="${defaultProvide.inputName}`);
const findHiddenSelectedNamespaceInput = () =>
@@ -111,10 +118,6 @@ describe('NewProjectUrlSelect component', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the root url as a label', () => {
wrapper = mountComponent();
@@ -177,13 +180,11 @@ describe('NewProjectUrlSelect component', () => {
});
it('focuses on the input when the dropdown is opened', async () => {
- wrapper = mountComponent({ mountFn: mount });
-
- const spy = jest.spyOn(findInput().vm, 'focusInput');
+ wrapper = mountComponent();
await showDropdown();
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(focusInputSpy).toHaveBeenCalledTimes(1);
});
it('renders expected dropdown items', async () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
index fc51825f15b..1545c52d7cb 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
@@ -8,20 +8,19 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
Some title
</p>
- <div>
- <glareachart-stub
- annotations=""
- data="[object Object],[object Object]"
- height="300"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- thresholds=""
- width="0"
- />
- </div>
+ <glareachart-stub
+ annotations=""
+ data="[object Object],[object Object]"
+ height="300"
+ legendaveragetext="Avg"
+ legendcurrenttext="Current"
+ legendlayout="inline"
+ legendmaxtext="Max"
+ legendmintext="Min"
+ option="[object Object]"
+ responsive=""
+ thresholds=""
+ width="auto"
+ />
</div>
`;
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index d8876349c5e..94f421239da 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -49,10 +49,6 @@ describe('ProjectsPipelinesChartsApp', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js
index 2b523467379..5fc121b5c9f 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js
@@ -28,11 +28,6 @@ describe('CiCdAnalyticsAreaChart', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
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 8fb59f38ee1..ab2a12219e5 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -37,11 +37,6 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
await waitForPromises();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.findComponent(StatisticsList);
diff --git a/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js b/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js
index 57a864cb2c4..24dbc628ce6 100644
--- a/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js
@@ -21,10 +21,6 @@ describe('StatisticsList', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the counts data with labels', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/projects/prune_unreachable_objects_button_spec.js b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
index b345f264ca7..012b19ea3d3 100644
--- a/spec/frontend/projects/prune_unreachable_objects_button_spec.js
+++ b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
@@ -22,16 +22,11 @@ describe('Project remove modal', () => {
wrapper = shallowMountExtended(PruneObjectsButton, {
propsData: defaultProps,
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('intialized', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
index 11f219c1f90..6d3317a5f78 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
@@ -8,10 +8,10 @@ import BranchDropdown, {
import createMockApollo from 'helpers/mock_apollo_helper';
import branchesQuery from '~/projects/settings/branch_rules/queries/branches.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Branch dropdown', () => {
let wrapper;
@@ -46,10 +46,6 @@ describe('Branch dropdown', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a GlDropdown component with the correct props', () => {
expect(findGlDropdown().props()).toMatchObject({ text: value });
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
index 21e63fdb24d..e9982872e03 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
@@ -24,10 +24,6 @@ describe('Edit branch rule', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('gets the branch param from url', () => {
expect(getParameterByName).toHaveBeenCalledWith('branch');
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
index ee90ff8318f..14edaf31a1f 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
@@ -26,10 +26,6 @@ describe('Branch Protections', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a heading', () => {
expect(findHeading().text()).toBe(i18n.protections);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
index b5fdc46d600..ca561ef87ec 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
@@ -24,10 +24,6 @@ describe('Merge Protections', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a form group with the correct label', () => {
expect(findFormGroup().text()).toContain(i18n.allowedToMerge);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
index 60bb7a51dcb..82998640f17 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
@@ -24,10 +24,6 @@ describe('Push Protections', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a form group with the correct label', () => {
expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index 714e0df596e..077995ab6e4 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -9,6 +9,10 @@ import Protection from '~/projects/settings/branch_rules/components/view/protect
import {
I18N,
ALL_BRANCHES_WILDCARD,
+ REQUIRED_ICON,
+ NOT_REQUIRED_ICON,
+ REQUIRED_ICON_CLASS,
+ NOT_REQUIRED_ICON_CLASS,
} from '~/projects/settings/branch_rules/components/view/constants';
import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
@@ -19,7 +23,7 @@ import {
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
- mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=main'),
+ mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=%5Emain%24'),
joinPaths: jest.fn(),
}));
@@ -39,12 +43,13 @@ describe('View branch rules', () => {
let fakeApollo;
const projectPath = 'test/testing';
const protectedBranchesPath = 'protected/branches';
- const branchProtectionsMockRequestHandler = jest
- .fn()
- .mockResolvedValue(branchProtectionsMockResponse);
+ const branchProtectionsMockRequestHandler = (response = branchProtectionsMockResponse) =>
+ jest.fn().mockResolvedValue(response);
- const createComponent = async () => {
- fakeApollo = createMockApollo([[branchRulesQuery, branchProtectionsMockRequestHandler]]);
+ const createComponent = async (mockResponse) => {
+ fakeApollo = createMockApollo([
+ [branchRulesQuery, branchProtectionsMockRequestHandler(mockResponse)],
+ ]);
wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo,
@@ -57,13 +62,13 @@ describe('View branch rules', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const findBranchName = () => wrapper.findByTestId('branch');
const findBranchTitle = () => wrapper.findByTestId('branch-title');
const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle);
const findBranchProtections = () => wrapper.findAllComponents(Protection);
- const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
+ const findForcePushIcon = () => wrapper.findByTestId('force-push-icon');
+ const findForcePushTitle = (title) => wrapper.findByText(title);
+ const findForcePushDescription = () => wrapper.findByText(I18N.forcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
const findMatchingBranchesLink = () =>
@@ -94,9 +99,12 @@ describe('View branch rules', () => {
});
it('renders matching branches link', () => {
+ const mergeUrlParams = jest.spyOn(util, 'mergeUrlParams');
const matchingBranchesLink = findMatchingBranchesLink();
+
+ expect(mergeUrlParams).toHaveBeenCalledWith({ state: 'all', search: `^main$` }, '');
expect(matchingBranchesLink.exists()).toBe(true);
- expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=main');
+ expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=%5Emain%24');
});
it('renders a branch protection title', () => {
@@ -123,9 +131,23 @@ describe('View branch rules', () => {
});
});
- it('renders force push protection', () => {
- expect(findForcePushTitle().exists()).toBe(true);
- });
+ it.each`
+ allowForcePush | iconName | iconClass | title
+ ${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.allowForcePushTitle}
+ ${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotAllowForcePushTitle}
+ `(
+ 'renders force push section with the correct icon, title and description',
+ async ({ allowForcePush, iconName, iconClass, title }) => {
+ const mockResponse = branchProtectionsMockResponse;
+ mockResponse.data.project.branchRules.nodes[0].branchProtection.allowForcePush = allowForcePush;
+ await createComponent(mockResponse);
+
+ expect(findForcePushIcon().props('name')).toBe(iconName);
+ expect(findForcePushIcon().attributes('class')).toBe(iconClass);
+ expect(findForcePushTitle(title).exists()).toBe(true);
+ expect(findForcePushDescription().exists()).toBe(true);
+ },
+ );
it('renders a branch protection component for merge rules', () => {
expect(findBranchProtections().at(1).props()).toMatchObject({
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
index a98b156f94e..1bfd04e10a1 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
@@ -18,8 +18,6 @@ describe('Branch rule protection row', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const findTitle = () => wrapper.findByText(protectionRowPropsMock.title);
const findAvatarsInline = () => wrapper.findComponent(GlAvatarsInline);
const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink);
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
index caf967b4257..f10d8d6d770 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
@@ -16,8 +16,6 @@ describe('Branch rule protection', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const findCard = () => wrapper.findComponent(GlCard);
const findHeader = () => wrapper.findByText(protectionPropsMock.header);
const findLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
index ca9a72663d2..c1412d01b53 100644
--- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js
+++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
@@ -19,10 +19,6 @@ describe('projects/settings/components/default_branch_selector', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
buildWrapper();
});
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 26297d0c3ff..f3e536de703 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -89,15 +89,12 @@ describe('Access Level Dropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggleLabel = () => findDropdown().props('text');
const findAllDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findAllDropdownHeaders = () => findDropdown().findAllComponents(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDeployKeyDropdownItem = () => wrapper.findByTestId('deploy_key-dropdown-item');
const findDropdownItemWithText = (items, text) =>
items.filter((item) => item.text().includes(text)).at(0);
@@ -142,6 +139,21 @@ describe('Access Level Dropdown', () => {
it('renders dropdown item for each access level type', () => {
expect(findAllDropdownItems()).toHaveLength(12);
});
+
+ it.each`
+ accessLevel | shouldRenderDeployKeyItems
+ ${ACCESS_LEVELS.PUSH} | ${true}
+ ${ACCESS_LEVELS.CREATE} | ${true}
+ ${ACCESS_LEVELS.MERGE} | ${false}
+ `(
+ 'conditionally renders deploy keys based on access levels',
+ async ({ accessLevel, shouldRenderDeployKeyItems }) => {
+ createComponent({ accessLevel });
+ await waitForPromises();
+
+ expect(findDeployKeyDropdownItem().exists()).toBe(shouldRenderDeployKeyItems);
+ },
+ );
});
describe('toggleLabel', () => {
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 f82ad80135e..0ec0e981d65 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -9,7 +9,7 @@ import SharedRunnersToggleComponent from '~/projects/settings/components/shared_
const TEST_UPDATE_PATH = '/test/update_shared_runners';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('projects/settings/components/shared_runners', () => {
let wrapper;
@@ -41,8 +41,6 @@ describe('projects/settings/components/shared_runners', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index e091f3e25c3..d8c2cf83f38 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -31,10 +31,6 @@ describe('Transfer project form', () => {
const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => {
createComponent();
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 56b39f04580..dd534bec25d 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -7,7 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
branchRulesMockResponse,
appProvideMock,
@@ -22,7 +22,7 @@ import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/settings_panels');
jest.mock('~/lib/utils/common_utils');
@@ -41,7 +41,7 @@ describe('Branch rules app', () => {
apolloProvider: fakeApollo,
provide: appProvideMock,
stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) },
- directives: { GlModal: createMockDirective() },
+ directives: { GlModal: createMockDirective('gl-modal') },
});
await waitForPromises();
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
index 8d0fd390e35..8bea84f4429 100644
--- 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
@@ -71,8 +71,10 @@ describe('Branch rule', () => {
});
it('renders a detail button with the correct href', () => {
+ const encodedBranchName = encodeURIComponent(branchRulePropsMock.name);
+
expect(findDetailsButton().attributes('href')).toBe(
- `${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`,
+ `${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`,
);
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index de7f6c8b88d..d169397241d 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -74,7 +74,7 @@ export const branchRuleProvideMock = {
};
export const branchRulePropsMock = {
- name: 'main',
+ name: 'branch-with-$speci@l-#-chars',
isDefault: true,
matchingBranchesCount: 1,
branchProtection: {
diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
index 8b8e7d1454d..4b94c179f74 100644
--- a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
+++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
@@ -64,7 +64,6 @@ describe('TopicsTokenSelector', () => {
});
afterEach(() => {
- wrapper.destroy();
div.remove();
input.remove();
});
diff --git a/spec/frontend/projects/settings/utils_spec.js b/spec/frontend/projects/settings/utils_spec.js
index 319aa4000b5..d85f43778b1 100644
--- a/spec/frontend/projects/settings/utils_spec.js
+++ b/spec/frontend/projects/settings/utils_spec.js
@@ -1,4 +1,5 @@
-import { getAccessLevels } from '~/projects/settings/utils';
+import { getAccessLevels, generateRefDestinationPath } from '~/projects/settings/utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { pushAccessLevelsMockResponse, pushAccessLevelsMockResult } from './mock_data';
describe('Utils', () => {
@@ -8,4 +9,25 @@ describe('Utils', () => {
expect(pushAccessLevels).toEqual(pushAccessLevelsMockResult);
});
});
+
+ describe('generateRefDestinationPath', () => {
+ const projectRootPath = 'http://test.host/root/Project1';
+ const settingsCi = '-/settings/ci_cd';
+
+ it.each`
+ currentPath | selectedRef | result
+ ${`${projectRootPath}`} | ${undefined} | ${`${projectRootPath}`}
+ ${`${projectRootPath}`} | ${'test'} | ${`${projectRootPath}`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'test'} | ${`${projectRootPath}/${settingsCi}?ref=test`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'branch-hyphen'} | ${`${projectRootPath}/${settingsCi}?ref=branch-hyphen`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'test/branch'} | ${`${projectRootPath}/${settingsCi}?ref=test%2Fbranch`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'test/branch-hyphen'} | ${`${projectRootPath}/${settingsCi}?ref=test%2Fbranch-hyphen`}
+ `(
+ 'generates the correct destination path for the `$selectedRef` ref and current url $currentPath by outputting $result',
+ ({ currentPath, selectedRef, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(selectedRef)).toBe(result);
+ },
+ );
+ });
});
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 5fc9f9ba629..4d0d2191176 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
@@ -41,7 +41,6 @@ describe('ServiceDeskRoot', () => {
afterEach(() => {
axiosMock.restore();
- wrapper.destroy();
if (spy) {
spy.mockRestore();
}
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index 6576ce70d60..1d0faebbcb2 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -41,10 +41,6 @@ describe('TerraformNotificationBanner', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user has already dismissed the banner', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index b4029d94980..4141d000a1c 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_URL = `${TEST_HOST}/url`;
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
@@ -149,7 +149,7 @@ describe('ProtectedBranchEdit', () => {
toggle.click();
});
- it('flashes error', async () => {
+ it('alerts error', async () => {
await axios.waitForAll();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap b/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap
deleted file mode 100644
index 5053778369e..00000000000
--- a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap
+++ /dev/null
@@ -1,80 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Ref selector component footer slot passes the expected slot props 1`] = `
-Object {
- "isLoading": false,
- "matches": Object {
- "branches": Object {
- "error": null,
- "list": Array [
- Object {
- "default": false,
- "name": "add_images_and_changes",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "conflict-contains-conflict-markers",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "deleted-image-test",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "diff-files-image-to-symlink",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "diff-files-symlink-to-image",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "markdown",
- "value": undefined,
- },
- Object {
- "default": true,
- "name": "master",
- "value": undefined,
- },
- ],
- "totalCount": 123,
- },
- "commits": Object {
- "error": null,
- "list": Array [
- Object {
- "name": "b83d6e39",
- "subtitle": "Merge branch 'branch-merged' into 'master'",
- "value": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
- },
- ],
- "totalCount": 1,
- },
- "tags": Object {
- "error": null,
- "list": Array [
- Object {
- "name": "v1.1.1",
- "value": undefined,
- },
- Object {
- "name": "v1.1.0",
- "value": undefined,
- },
- Object {
- "name": "v1.0.0",
- "value": undefined,
- },
- ],
- "totalCount": 456,
- },
- },
- "query": "abcd1234",
-}
-`;
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 40d3a291074..6b90827f9c2 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -4,9 +4,9 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge, last } from 'lodash';
import Vuex from 'vuex';
+import tags from 'test_fixtures/api/tags/tags.json';
import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json';
-import tags from 'test_fixtures/api/tags/tags.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import {
@@ -33,6 +33,8 @@ describe('Ref selector component', () => {
const fixtures = { branches, tags, commit };
const projectId = '8';
+ const totalBranchesCount = 123;
+ const totalTagsCount = 456;
let wrapper;
let branchesApiCallSpy;
@@ -69,10 +71,14 @@ describe('Ref selector component', () => {
branchesApiCallSpy = jest
.fn()
- .mockReturnValue([HTTP_STATUS_OK, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
+ .mockReturnValue([
+ HTTP_STATUS_OK,
+ fixtures.branches,
+ { [X_TOTAL_HEADER]: totalBranchesCount },
+ ]);
tagsApiCallSpy = jest
.fn()
- .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
+ .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: totalTagsCount }]);
commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
@@ -690,7 +696,46 @@ describe('Ref selector component', () => {
// is updated. For the sake of this test, we'll just test the last call, which
// represents the final state of the slot props.
const lastCallProps = last(createFooter.mock.calls)[0];
- expect(lastCallProps).toMatchSnapshot();
+ expect(lastCallProps.isLoading).toBe(false);
+ expect(lastCallProps.query).toBe('abcd1234');
+
+ const branchesList = fixtures.branches.map((branch) => {
+ return {
+ default: branch.default,
+ name: branch.name,
+ };
+ });
+
+ const commitsList = [
+ {
+ name: fixtures.commit.short_id,
+ subtitle: fixtures.commit.title,
+ value: fixtures.commit.id,
+ },
+ ];
+
+ const tagsList = fixtures.tags.map((tag) => {
+ return {
+ name: tag.name,
+ };
+ });
+
+ const expectedMatches = {
+ branches: {
+ list: branchesList,
+ totalCount: totalBranchesCount,
+ },
+ commits: {
+ list: commitsList,
+ totalCount: 1,
+ },
+ tags: {
+ list: tagsList,
+ totalCount: totalTagsCount,
+ },
+ };
+
+ expect(lastCallProps.matches).toMatchObject(expectedMatches);
});
});
});
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index bd61e4537f9..dcb6a3293a6 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -16,6 +16,7 @@ import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { ValidationResult } from '~/lib/utils/ref_validator';
const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release;
const originalMilestones = originalRelease.milestones;
@@ -58,6 +59,7 @@ describe('Release edit/new component', () => {
assets: {
links: [],
},
+ tagNameValidation: new ValidationResult(),
}),
formattedReleaseNotes: () => 'these notes are formatted',
};
@@ -101,11 +103,6 @@ describe('Release edit/new component', () => {
release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSubmitButton = () => wrapper.find('button[type=submit]');
const findForm = () => wrapper.find('form');
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index ef3bd5ca873..7a0e9fb7326 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { historyPushState } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
import ReleasesIndexApp from '~/releases/components/app_index.vue';
@@ -20,7 +20,7 @@ import { deleteReleaseSessionKey } from '~/releases/util';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
let mockQueryParams;
jest.mock('~/lib/utils/common_utils', () => ({
@@ -114,7 +114,7 @@ describe('app_index.vue', () => {
const toDescription = (bool) => (bool ? 'does' : 'does not');
describe.each`
- description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
+ description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | alertMessage | releaseCount | pagination
${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false}
${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
@@ -134,7 +134,7 @@ describe('app_index.vue', () => {
fullResponseFn,
loadingIndicator,
emptyState,
- flashMessage,
+ alertMessage,
releaseCount,
pagination,
}) => {
@@ -154,9 +154,9 @@ describe('app_index.vue', () => {
expect(findEmptyState().exists()).toBe(emptyState);
});
- it(`${toDescription(flashMessage)} show a flash message`, async () => {
+ it(`${toDescription(alertMessage)} show a flash message`, async () => {
await waitForPromises();
- if (flashMessage) {
+ if (alertMessage) {
expect(createAlert).toHaveBeenCalledWith({
message: ReleasesIndexApp.i18n.errorMessage,
captureError: true,
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index efe72e8000a..942280cb6a2 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo';
import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/releases/release_notification_service');
Vue.use(VueApollo);
@@ -33,11 +33,6 @@ describe('Release show component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findLoadingSkeleton = () => wrapper.findComponent(ReleaseSkeletonLoader);
const findReleaseBlock = () => wrapper.findComponent(ReleaseBlock);
@@ -54,13 +49,13 @@ describe('Release show component', () => {
};
const expectNoFlash = () => {
- it('does not show a flash message', () => {
+ it('does not show an alert message', () => {
expect(createAlert).not.toHaveBeenCalled();
});
};
const expectFlashWithMessage = (message) => {
- it(`shows a flash message that reads "${message}"`, () => {
+ it(`shows an alert message that reads "${message}"`, () => {
expect(createAlert).toHaveBeenCalledWith({
message,
captureError: true,
@@ -152,7 +147,7 @@ describe('Release show component', () => {
beforeEach(async () => {
// As we return a release as `null`, Apollo also throws an error to the console
// about the missing field. We need to suppress console.error in order to check
- // that flash message was called
+ // that alert message was called
// eslint-disable-next-line no-console
console.error = jest.fn();
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index b1e9d8d1256..8eee9acd808 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -60,11 +60,6 @@ describe('Release edit component', () => {
release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with a basic store state', () => {
beforeEach(() => {
factory();
diff --git a/spec/frontend/releases/components/confirm_delete_modal_spec.js b/spec/frontend/releases/components/confirm_delete_modal_spec.js
index f7c526c1ced..b4699302779 100644
--- a/spec/frontend/releases/components/confirm_delete_modal_spec.js
+++ b/spec/frontend/releases/components/confirm_delete_modal_spec.js
@@ -42,10 +42,6 @@ describe('~/releases/components/confirm_delete_modal.vue', () => {
factory();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('button', () => {
it('should open the modal on click', async () => {
await wrapper.findByRole('button', { name: 'Delete' }).trigger('click');
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 69443cb7a11..42eac31e5ac 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -27,10 +27,6 @@ describe('Evidence Block', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the evidence icon', () => {
expect(wrapper.findComponent(GlIcon).props('name')).toBe('review-list');
});
diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js
index 3ac75e138ee..c8cdf9cb951 100644
--- a/spec/frontend/releases/components/issuable_stats_spec.js
+++ b/spec/frontend/releases/components/issuable_stats_spec.js
@@ -34,11 +34,6 @@ describe('~/releases/components/issuable_stats.vue', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
createComponent();
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 19b41d05a44..12e3807c9fa 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -33,11 +33,6 @@ describe('Release block footer', () => {
release = cloneDeep(originalRelease);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const commitInfoSection = () => wrapper.find('.js-commit-info');
const commitInfoSectionLink = () => commitInfoSection().findComponent(GlLink);
const tagInfoSection = () => wrapper.find('.js-tag-info');
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index fc421776d60..dd39a1bce53 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -25,10 +25,6 @@ describe('Release block header', () => {
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().findComponent(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
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 541d487091c..b8030ae1fd2 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -25,11 +25,6 @@ describe('Release block milestone info', () => {
milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const milestoneProgressBarContainer = () => wrapper.find('.js-milestone-progress-bar-container');
const milestoneListContainer = () => wrapper.find('.js-milestone-list-container');
const issuesContainer = () => wrapper.find('[data-testid="issue-stats"]');
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index f1b8554fbc3..3355b5ab2c3 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -39,10 +39,6 @@ describe('Release block', () => {
release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with default props', () => {
beforeEach(() => factory(release));
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index 59be808c802..923d84ae2b3 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -29,10 +29,6 @@ describe('releases_pagination.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const singlePageInfo = {
hasPreviousPage: false,
hasNextPage: false,
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index c6e1846d252..92199896ab4 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -17,10 +17,6 @@ describe('releases_sort.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSorting = () => wrapper.findComponent(GlSorting);
const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
const findReleasedDateItem = () =>
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index 8105aa4f6f2..0e896eb645c 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -37,11 +37,6 @@ describe('releases/components/tag_field_existing', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
it('shows the tag name', () => {
createComponent();
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index fcba0da3462..2508495429c 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -9,6 +9,7 @@ import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { i18n } from '~/releases/constants';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_TAG_MESSAGE = 'Test tag message';
@@ -81,7 +82,6 @@ describe('releases/components/tag_field_new', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
@@ -210,9 +210,7 @@ describe('releases/components/tag_field_new', () => {
store.state.editNew.existingRelease = {};
await expectValidationMessageToBe('shown');
- expect(findTagNameFormGroup().text()).toContain(
- __('Selected tag is already in use. Choose another option.'),
- );
+ expect(findTagNameFormGroup().text()).toContain(i18n.tagIsAlredyInUseMessage);
});
});
@@ -222,7 +220,7 @@ describe('releases/components/tag_field_new', () => {
findTagNameDropdown().vm.$emit('hide');
await expectValidationMessageToBe('shown');
- expect(findTagNameFormGroup().text()).toContain(__('Tag name is required.'));
+ expect(findTagNameFormGroup().text()).toContain(i18n.tagNameIsRequiredMessage);
});
});
});
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
index 85a40f02c53..8509c347291 100644
--- a/spec/frontend/releases/components/tag_field_spec.js
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -24,11 +24,6 @@ describe('releases/components/tag_field', () => {
const findTagFieldNew = () => wrapper.findComponent(TagFieldNew);
const findTagFieldExisting = () => wrapper.findComponent(TagFieldExisting);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when an existing release is being edited', () => {
beforeEach(() => {
createComponent({ isExistingRelease: true });
diff --git a/spec/frontend/releases/release_notification_service_spec.js b/spec/frontend/releases/release_notification_service_spec.js
index 2344d4b929a..a90bfa3dcbd 100644
--- a/spec/frontend/releases/release_notification_service_spec.js
+++ b/spec/frontend/releases/release_notification_service_spec.js
@@ -2,9 +2,9 @@ import {
popCreateReleaseNotification,
putCreateReleaseNotification,
} from '~/releases/release_notification_service';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('~/releases/release_notification_service', () => {
const projectPath = 'test-project-path';
@@ -35,7 +35,7 @@ describe('~/releases/release_notification_service', () => {
expect(item).toBe(null);
});
- it('should create a flash message', () => {
+ it('should create an alert message', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: `Release ${releaseName} has been successfully created.`,
@@ -49,7 +49,7 @@ describe('~/releases/release_notification_service', () => {
popCreateReleaseNotification(projectPath);
});
- it('should not create a flash message', () => {
+ it('should not create an alert message', () => {
expect(createAlert).toHaveBeenCalledTimes(0);
});
});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index ca3b2d5f734..2fca3396a1f 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import { getTag } from '~/api/tags_api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { ASSET_LINK_TYPE } from '~/releases/constants';
@@ -21,7 +21,7 @@ import {
jest.mock('~/api/tags_api');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/releases/release_notification_service');
@@ -154,7 +154,7 @@ describe('Release edit/new actions', () => {
]);
});
- it(`shows a flash message`, () => {
+ it(`shows an alert message`, () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
@@ -380,7 +380,7 @@ describe('Release edit/new actions', () => {
]);
});
- it(`shows a flash message`, () => {
+ it(`shows an alert message`, () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
@@ -406,7 +406,7 @@ describe('Release edit/new actions', () => {
]);
});
- it(`shows a flash message`, () => {
+ it(`shows an alert message`, () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
@@ -538,7 +538,7 @@ describe('Release edit/new actions', () => {
expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -558,7 +558,7 @@ describe('Release edit/new actions', () => {
]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -711,7 +711,7 @@ describe('Release edit/new actions', () => {
expect(commit.mock.calls).toContainEqual([types.RECEIVE_SAVE_RELEASE_ERROR, error]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.deleteRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -747,7 +747,7 @@ describe('Release edit/new actions', () => {
]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.deleteRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -778,7 +778,7 @@ describe('Release edit/new actions', () => {
expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
});
- it('creates a flash on error', async () => {
+ it('creates an alert on error', async () => {
error = new Error();
getTag.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 f8b87ec71dc..649e772f956 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -1,5 +1,16 @@
import { s__ } from '~/locale';
import * as getters from '~/releases/stores/modules/edit_new/getters';
+import { i18n } from '~/releases/constants';
+import { validateTag, ValidationResult } from '~/lib/utils/ref_validator';
+
+jest.mock('~/lib/utils/ref_validator', () => {
+ const original = jest.requireActual('~/lib/utils/ref_validator');
+ return {
+ __esModule: true,
+ ValidationResult: original.ValidationResult,
+ validateTag: jest.fn(() => new original.ValidationResult()),
+ };
+});
describe('Release edit/new getters', () => {
describe('releaseLinksToCreate', () => {
@@ -59,23 +70,23 @@ describe('Release edit/new getters', () => {
});
describe('validationErrors', () => {
+ const validState = {
+ release: {
+ tagName: 'test-tag-name',
+ assets: {
+ links: [
+ { id: 1, url: 'https://example.com/valid', name: 'Link 1' },
+ { id: 2, url: '', name: '' },
+ { id: 3, url: '', name: ' ' },
+ { id: 4, url: ' ', name: '' },
+ { id: 5, url: ' ', name: ' ' },
+ ],
+ },
+ },
+ };
describe('when the form is valid', () => {
+ const state = validState;
it('returns no validation errors', () => {
- const state = {
- release: {
- tagName: 'test-tag-name',
- assets: {
- links: [
- { id: 1, url: 'https://example.com/valid', name: 'Link 1' },
- { id: 2, url: '', name: '' },
- { id: 3, url: '', name: ' ' },
- { id: 4, url: ' ', name: '' },
- { id: 5, url: ' ', name: ' ' },
- ],
- },
- },
- };
-
const expectedErrors = {
assets: {
links: {
@@ -88,7 +99,27 @@ describe('Release edit/new getters', () => {
},
};
- expect(getters.validationErrors(state)).toEqual(expectedErrors);
+ expect(getters.validationErrors(state).assets).toEqual(expectedErrors.assets);
+ expect(getters.validationErrors(state).tagNameValidation.isValid).toBe(true);
+ });
+ });
+
+ describe('when validating tag', () => {
+ const state = validState;
+ it('validateTag is called with right parameters', () => {
+ getters.validationErrors(state);
+ expect(validateTag).toHaveBeenCalledWith(state.release.tagName);
+ });
+
+ it('validation error is correctly returned', () => {
+ const validationError = new ValidationResult();
+ const errorText = 'Tag format validation error';
+ validationError.addValidationError(errorText);
+ validateTag.mockReturnValue(validationError);
+
+ const result = getters.validationErrors(state);
+ expect(validateTag).toHaveBeenCalledWith(state.release.tagName);
+ expect(result.tagNameValidation.validationErrors).toContain(errorText);
});
});
@@ -140,19 +171,17 @@ describe('Release edit/new getters', () => {
});
it('returns a validation error if the tag name is empty', () => {
- const expectedErrors = {
- isTagNameEmpty: true,
- };
-
- expect(actualErrors).toMatchObject(expectedErrors);
+ expect(actualErrors.tagNameValidation.isValid).toBe(false);
+ expect(actualErrors.tagNameValidation.validationErrors).toContain(
+ i18n.tagNameIsRequiredMessage,
+ );
});
it('returns a validation error if the tag has an existing release', () => {
- const expectedErrors = {
- existingRelease: true,
- };
-
- expect(actualErrors).toMatchObject(expectedErrors);
+ expect(actualErrors.tagNameValidation.isValid).toBe(false);
+ expect(actualErrors.tagNameValidation.validationErrors).toContain(
+ i18n.tagIsAlredyInUseMessage,
+ );
});
it('returns a validation error if links share a URL', () => {
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
index e56975d021a..22ef552c2f9 100644
--- a/spec/frontend/repository/commits_service_spec.js
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
import { refWithSpecialCharMock } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('commits service', () => {
let mock;
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 33a85c04fcf..96dedd54126 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -38,10 +38,6 @@ describe('BlobButtonGroup component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findDeleteButton = () => wrapper.findByTestId('delete');
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 03a8ee6ac5d..a588251c4bd 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -175,7 +175,6 @@ describe('Blob content viewer component', () => {
});
afterEach(() => {
- wrapper.destroy();
mockAxios.reset();
});
@@ -482,11 +481,6 @@ describe('Blob content viewer component', () => {
repository: { empty },
} = projectMock;
- afterEach(() => {
- delete gon.current_user_id;
- delete gon.current_username;
- });
-
it('renders component', async () => {
window.gon.current_user_id = 1;
window.gon.current_username = 'root';
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
index 0d52542397f..3ced5f6c4d2 100644
--- a/spec/frontend/repository/components/blob_controls_spec.js
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -50,8 +50,6 @@ describe('Blob controls component', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
it('renders a find button with the correct href', () => {
expect(findFindButton().attributes('href')).toBe('find/file.js');
});
diff --git a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
index 599443bf862..b4f4b0058de 100644
--- a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
@@ -21,8 +21,6 @@ describe('LFS Viewer', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
it('renders the correct text', () => {
expect(wrapper.text()).toBe(
'This content could not be displayed because it is stored in LFS. You can download it instead.',
diff --git a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
index 51f3d31ec72..5d37692bf90 100644
--- a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
@@ -1,7 +1,6 @@
-import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NotebookViewer from '~/repository/components/blob_viewers/notebook_viewer.vue';
-import notebookLoader from '~/blob/notebook';
+import Notebook from '~/blob/notebook/notebook_viewer.vue';
jest.mock('~/blob/notebook');
@@ -17,24 +16,11 @@ describe('Notebook Viewer', () => {
});
};
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findNotebookWrapper = () => wrapper.findByTestId('notebook');
+ const findNotebook = () => wrapper.findComponent(Notebook);
beforeEach(() => createComponent());
- it('calls the notebook loader', () => {
- expect(notebookLoader).toHaveBeenCalledWith({
- el: wrapper.vm.$refs.viewer,
- relativeRawPath: ROOT_RELATIVE_PATH,
- });
- });
-
- it('renders a loading icon component', () => {
- expect(findLoadingIcon().props('size')).toBe('lg');
- });
-
- it('renders the notebook wrapper', () => {
- expect(findNotebookWrapper().exists()).toBe(true);
- expect(findNotebookWrapper().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ it('renders a Notebook component', () => {
+ expect(findNotebook().props('endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index c2f34f79f89..8b7a7d91125 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -42,10 +42,6 @@ describe('Repository breadcrumbs component', () => {
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
path | linkCount
${'/'} | ${1}
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
index b5996816ad8..9ca45bfb655 100644
--- a/spec/frontend/repository/components/delete_blob_modal_spec.js
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -49,10 +49,6 @@ describe('DeleteBlobModal', () => {
await findCommitTextarea().vm.$emit('input', commitText);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders Modal component', () => {
createComponent();
@@ -187,7 +183,7 @@ describe('DeleteBlobModal', () => {
});
it('disables submit button', async () => {
- expect(findModal().props('actionPrimary').attributes[0]).toEqual(
+ expect(findModal().props('actionPrimary').attributes).toEqual(
expect.objectContaining({ disabled: true }),
);
});
@@ -207,7 +203,7 @@ describe('DeleteBlobModal', () => {
});
it('enables submit button', async () => {
- expect(findModal().props('actionPrimary').attributes[0]).toEqual(
+ expect(findModal().props('actionPrimary').attributes).toEqual(
expect.objectContaining({ disabled: false }),
);
});
diff --git a/spec/frontend/repository/components/directory_download_links_spec.js b/spec/frontend/repository/components/directory_download_links_spec.js
index 72c4165c2e9..3739829c759 100644
--- a/spec/frontend/repository/components/directory_download_links_spec.js
+++ b/spec/frontend/repository/components/directory_download_links_spec.js
@@ -16,10 +16,6 @@ function factory(currentPath) {
}
describe('Repository directory download links component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
path
${'app'}
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
index f327a8cfae7..7a2b03a8d8f 100644
--- a/spec/frontend/repository/components/fork_info_spec.js
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -1,42 +1,75 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf, GlButton, GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import ForkInfo, { i18n } from '~/repository/components/fork_info.vue';
+import ConflictsModal from '~/repository/components/fork_sync_conflicts_modal.vue';
import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql';
+import syncForkMutation from '~/repository/mutations/sync_fork.mutation.graphql';
import { propsForkInfo } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ForkInfo component', () => {
let wrapper;
let mockResolver;
const forkInfoError = new Error('Something went wrong');
const projectId = 'gid://gitlab/Project/1';
+ const showMock = jest.fn();
+ const synchronizeFork = true;
Vue.use(VueApollo);
- const createCommitData = ({ ahead = 3, behind = 7 }) => {
+ const createForkDetailsData = (
+ forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
+ ) => {
return {
data: {
- project: { id: projectId, forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
+ project: { id: projectId, forkDetails },
},
};
};
- const createComponent = (props = {}, data = {}, isRequestFailed = false) => {
+ const createSyncForkDetailsData = (
+ forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
+ ) => {
+ return {
+ data: {
+ projectSyncFork: { details: forkDetails, errors: [] },
+ },
+ };
+ };
+
+ const createComponent = (props = {}, data = {}, mutationData = {}, isRequestFailed = false) => {
mockResolver = isRequestFailed
? jest.fn().mockRejectedValue(forkInfoError)
- : jest.fn().mockResolvedValue(createCommitData(data));
+ : jest.fn().mockResolvedValue(createForkDetailsData(data));
wrapper = shallowMountExtended(ForkInfo, {
- apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]),
+ apolloProvider: createMockApollo([
+ [forkDetailsQuery, mockResolver],
+ [syncForkMutation, jest.fn().mockResolvedValue(createSyncForkDetailsData(mutationData))],
+ ]),
propsData: { ...propsForkInfo, ...props },
- stubs: { GlSprintf },
+ stubs: {
+ GlSprintf,
+ GlButton,
+ ConflictsModal: stubComponent(ConflictsModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: { show: showMock },
+ }),
+ },
+ provide: {
+ glFeatures: {
+ synchronizeFork,
+ },
+ },
});
return waitForPromises();
};
@@ -44,6 +77,8 @@ describe('ForkInfo component', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findIcon = () => wrapper.findComponent(GlIcon);
+ const findUpdateForkButton = () => wrapper.findComponent(GlButton);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDivergenceMessage = () => wrapper.findByTestId('divergence-message');
const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
const findCompareLinks = () => findDivergenceMessage().findAllComponents(GlLink);
@@ -87,14 +122,50 @@ describe('ForkInfo component', () => {
expect(link.attributes('href')).toBe(propsForkInfo.sourcePath);
});
- it('renders unknown divergence message when divergence is unknown', async () => {
- await createComponent({}, { ahead: null, behind: null });
- expect(findDivergenceMessage().text()).toBe(i18n.unknown);
+ describe('Unknown divergence', () => {
+ beforeEach(async () => {
+ await createComponent(
+ {},
+ { ahead: null, behind: null, isSyncing: false, hasConflicts: false },
+ );
+ });
+
+ it('renders unknown divergence message when divergence is unknown', async () => {
+ expect(findDivergenceMessage().text()).toBe(i18n.unknown);
+ });
+
+ it('renders Update Fork button', async () => {
+ expect(findUpdateForkButton().exists()).toBe(true);
+ expect(findUpdateForkButton().text()).toBe(i18n.sync);
+ });
+ });
+
+ describe('Up to date divergence', () => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ });
+
+ it('renders up to date message when fork is up to date', async () => {
+ expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
+ });
+
+ it('does not render Update Fork button', async () => {
+ expect(findUpdateForkButton().exists()).toBe(false);
+ });
});
- it('renders up to date message when divergence is unknown', async () => {
- await createComponent({}, { ahead: 0, behind: 0 });
- expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
+ describe('Limited visibility project', () => {
+ beforeEach(async () => {
+ await createComponent({}, null);
+ });
+
+ it('renders limited visibility messsage when forkDetails are empty', async () => {
+ expect(findDivergenceMessage().text()).toBe(i18n.limitedVisibility);
+ });
+
+ it('does not render Update Fork button', async () => {
+ expect(findUpdateForkButton().exists()).toBe(false);
+ });
});
describe.each([
@@ -104,6 +175,7 @@ describe('ForkInfo component', () => {
message: '3 commits behind, 7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: propsForkInfo.aheadComparePath,
+ hasButton: true,
},
{
ahead: 7,
@@ -111,6 +183,7 @@ describe('ForkInfo component', () => {
message: '7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.aheadComparePath,
secondLink: '',
+ hasButton: false,
},
{
ahead: 0,
@@ -118,12 +191,13 @@ describe('ForkInfo component', () => {
message: '3 commits behind the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: '',
+ hasButton: true,
},
])(
'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
- ({ ahead, behind, message, firstLink, secondLink }) => {
+ ({ ahead, behind, message, firstLink, secondLink, hasButton }) => {
beforeEach(async () => {
- await createComponent({}, { ahead, behind });
+ await createComponent({}, { ahead, behind, isSyncing: false, hasConflicts: false });
});
it('displays correct text', () => {
@@ -138,9 +212,38 @@ describe('ForkInfo component', () => {
expect(links.at(1).attributes('href')).toBe(secondLink);
}
});
+
+ it('renders Update Fork button when fork is behind', () => {
+ expect(findUpdateForkButton().exists()).toBe(hasButton);
+ if (hasButton) {
+ expect(findUpdateForkButton().text()).toBe(i18n.sync);
+ }
+ });
},
);
+ describe('when sync is not possible due to conflicts', () => {
+ it('opens Conflicts Modal', async () => {
+ await createComponent({}, { ahead: 7, behind: 3, isSyncing: false, hasConflicts: true });
+ findUpdateForkButton().vm.$emit('click');
+ expect(showMock).toHaveBeenCalled();
+ });
+ });
+
+ describe('projectSyncFork mutation', () => {
+ it('changes button to have loading state', async () => {
+ await createComponent(
+ {},
+ { ahead: 0, behind: 3, isSyncing: false, hasConflicts: false },
+ { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false },
+ );
+ expect(findLoadingIcon().exists()).toBe(false);
+ findUpdateForkButton().vm.$emit('click');
+ await waitForPromises();
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
it('renders alert with error message when request fails', async () => {
await createComponent({}, {}, true);
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/repository/components/fork_suggestion_spec.js b/spec/frontend/repository/components/fork_suggestion_spec.js
index 36a48a3fdb8..a9e5c18c0a9 100644
--- a/spec/frontend/repository/components/fork_suggestion_spec.js
+++ b/spec/frontend/repository/components/fork_suggestion_spec.js
@@ -14,8 +14,6 @@ describe('ForkSuggestion component', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const { i18n } = ForkSuggestion;
const findMessage = () => wrapper.findByTestId('message');
const findForkButton = () => wrapper.findByTestId('fork');
diff --git a/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
new file mode 100644
index 00000000000..f97c970275b
--- /dev/null
+++ b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
@@ -0,0 +1,42 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ConflictsModal, { i18n } from '~/repository/components/fork_sync_conflicts_modal.vue';
+import { propsConflictsModal } from '../mock_data';
+
+describe('ConflictsModal', () => {
+ let wrapper;
+
+ function createComponent({ props = {} } = {}) {
+ wrapper = shallowMount(ConflictsModal, {
+ propsData: props,
+ stubs: { GlModal },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent({ props: propsConflictsModal });
+ });
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findInstructions = () => wrapper.findAll('[ data-testid="resolve-conflict-instructions"]');
+
+ it('renders a modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('passes title as a prop to a gl-modal component', () => {
+ expect(findModal().props().title).toBe(i18n.modalTitle);
+ });
+
+ it('renders a selection of markdown fields', () => {
+ expect(findInstructions().length).toBe(3);
+ });
+
+ it('renders a source url in a first intruction', () => {
+ expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourcePath);
+ });
+
+ it('renders default branch name in a first intruction', () => {
+ expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourceDefaultBranch);
+ });
+});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 7226e7baa36..f16edcb0b7c 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LastCommit from '~/repository/components/last_commit.vue';
+import SignatureBadge from '~/commit/components/signature_badge.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { refMock } from '../mock_data';
@@ -20,7 +21,7 @@ const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCommitRowDescription = () => wrapper.find('.commit-row-description');
-const findStatusBox = () => wrapper.find('.signature-badge');
+const findStatusBox = () => wrapper.findComponent(SignatureBadge);
const findItemTitle = () => wrapper.find('.item-title');
const defaultPipelineEdges = [
@@ -56,7 +57,7 @@ const createCommitData = ({
pipelineEdges = defaultPipelineEdges,
author = defaultAuthor,
descriptionHtml = '',
- signatureHtml = null,
+ signature = null,
message = defaultMessage,
}) => {
return {
@@ -84,7 +85,7 @@ const createCommitData = ({
authorName: 'Test',
authorGravatar: 'https://test.com',
author,
- signatureHtml,
+ signature,
pipelines: {
__typename: 'PipelineConnection',
edges: pipelineEdges,
@@ -110,11 +111,13 @@ const createComponent = async (data = {}) => {
apolloProvider: createMockApollo([[pathLastCommitQuery, mockResolver]]),
propsData: { currentPath },
mixins: [{ data: () => ({ ref: refMock }) }],
+ stubs: {
+ SignatureBadge,
+ },
});
};
afterEach(() => {
- wrapper.destroy();
mockResolver = null;
});
@@ -204,23 +207,19 @@ describe('Repository last commit component', () => {
});
it('renders the signature HTML as returned by the backend', async () => {
+ const signatureResponse = {
+ __typename: 'GpgSignature',
+ gpgKeyPrimaryKeyid: 'xxx',
+ verificationStatus: 'VERIFIED',
+ };
createComponent({
- signatureHtml: `<a
- class="btn signature-badge"
- data-content="signature-content"
- data-html="true"
- data-placement="top"
- data-title="signature-title"
- data-toggle="popover"
- role="button"
- tabindex="0"
- ><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
+ signature: {
+ ...signatureResponse,
+ },
});
await waitForPromises();
- expect(findStatusBox().html()).toBe(
- `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0"><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
- );
+ expect(findStatusBox().props()).toMatchObject({ signature: signatureResponse });
});
it('sets correct CSS class if the commit message is empty', async () => {
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index 4e5c9a685c4..c920159375f 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -4,12 +4,12 @@ import { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
@@ -76,10 +76,6 @@ describe('NewDirectoryModal', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders modal component', () => {
createComponent();
@@ -185,10 +181,10 @@ describe('NewDirectoryModal', () => {
it('disables submit button', async () => {
await fillForm({ dirName: '', branchName: '', commitMessage: '' });
- expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true);
+ expect(findModal().props('actionPrimary').attributes.disabled).toBe(true);
});
- it('creates a flash error', async () => {
+ it('creates an alert error', async () => {
mock.onPost(initialProps.path).timeout();
await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
diff --git a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
deleted file mode 100644
index 48a4feca1e5..00000000000
--- a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Repository file preview component renders file HTML 1`] = `
-<article
- class="file-holder limited-width-container readme-holder"
->
- <div
- class="js-file-title file-title-flex-parent"
- >
- <div
- class="file-header-content"
- >
- <gl-icon-stub
- name="doc-text"
- size="16"
- />
-
- <gl-link-stub
- href="http://test.com"
- >
- <strong>
- README.md
- </strong>
- </gl-link-stub>
- </div>
- </div>
-
- <div
- class="blob-viewer"
- data-qa-selector="blob_viewer_content"
- itemprop="about"
- >
- <div>
- <div
- class="blob"
- >
- test
- </div>
- </div>
- </div>
-</article>
-`;
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index d4c746b67d6..8a88c5b9c61 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -1,77 +1,60 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import waitForPromises from 'helpers/wait_for_promises';
import Preview from '~/repository/components/preview/index.vue';
+const PROPS_DATA = {
+ blob: {
+ webPath: 'http://test.com',
+ name: 'README.md',
+ },
+};
+
+const MOCK_README_DATA = {
+ __typename: 'ReadmeFile',
+ html: '<div class="blob">test</div>',
+};
+
jest.mock('~/lib/utils/common_utils');
-let vm;
-let $apollo;
+Vue.use(VueApollo);
+
+let wrapper;
+let mockApollo;
+let mockReadmeData;
-function factory(blob, loading) {
- $apollo = {
- queries: {
- readme: {
- query: jest.fn().mockReturnValue(Promise.resolve({})),
- loading,
- },
- },
- };
+const mockResolvers = {
+ Query: {
+ readme: () => mockReadmeData(),
+ },
+};
- vm = shallowMount(Preview, {
- propsData: {
- blob,
- },
- mocks: {
- $apollo,
- },
+function createComponent() {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ return shallowMount(Preview, {
+ propsData: PROPS_DATA,
+ apolloProvider: mockApollo,
});
}
describe('Repository file preview component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
- it('renders file HTML', async () => {
- factory({
- webPath: 'http://test.com',
- name: 'README.md',
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ readme: { html: '<div class="blob">test</div>' } });
-
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ beforeEach(() => {
+ mockReadmeData = jest.fn();
+ wrapper = createComponent();
+ mockReadmeData.mockResolvedValue(MOCK_README_DATA);
});
it('handles hash after render', async () => {
- factory({
- webPath: 'http://test.com',
- name: 'README.md',
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ readme: { html: '<div class="blob">test</div>' } });
-
- await nextTick();
+ await waitForPromises();
expect(handleLocationHash).toHaveBeenCalled();
});
it('renders loading icon', async () => {
- factory(
- {
- webPath: 'http://test.com',
- name: 'README.md',
- },
- true,
- );
-
- await nextTick();
- expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.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 8b987551b33..f7be367887c 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -88,10 +88,6 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit
const findTableRows = () => vm.findAllComponents(TableRow);
describe('Repository table component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
path | ref
${'/'} | ${'main'}
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
index 03fb4242e40..77822a148b7 100644
--- a/spec/frontend/repository/components/table/parent_row_spec.js
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -26,10 +26,6 @@ function factory(path, loadingPath) {
}
describe('Repository parent row component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
path | to
${'app'} | ${'/-/tree/main/'}
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 5d9138ab9cd..055616d6e8e 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -28,7 +28,7 @@ function factory(propsData = {}) {
rowNumber: 123,
},
directives: {
- GlHoverLoad: createMockDirective(),
+ GlHoverLoad: createMockDirective('gl-hover-load'),
},
mocks: {
$router,
@@ -47,10 +47,6 @@ describe('Repository table row component', () => {
const findRouterLink = () => vm.findComponent(RouterLinkStub);
const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver);
- afterEach(() => {
- vm.destroy();
- });
-
it('renders table row', async () => {
factory({
id: '1',
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index f694c8e9166..9597d8a7b77 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,12 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from 'jh_else_ce/repository/components/tree_content.vue';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { i18n } from '~/repository/constants';
import { graphQLErrors } from '../mock_data';
@@ -15,15 +14,15 @@ jest.mock('~/repository/commits_service', () => ({
isRequested: jest.fn(),
resetRequestedCommits: jest.fn(),
}));
-jest.mock('~/flash');
+jest.mock('~/alert');
let vm;
let $apollo;
const mockResponse = jest.fn().mockReturnValue(Promise.resolve({ data: {} }));
-function factory(path, appoloMockResponse = mockResponse) {
+function factory(path, apolloMockResponse = mockResponse) {
$apollo = {
- query: appoloMockResponse,
+ query: apolloMockResponse,
};
vm = shallowMount(TreeContent, {
@@ -33,22 +32,12 @@ function factory(path, appoloMockResponse = mockResponse) {
mocks: {
$apollo,
},
- provide: {
- glFeatures: {
- increasePageSizeExponentially: true,
- paginatedTreeGraphqlQuery: true,
- },
- },
});
}
describe('Repository table component', () => {
const findFileTable = () => vm.findComponent(FileTable);
- afterEach(() => {
- vm.destroy();
- });
-
it('renders file preview', async () => {
factory('/');
@@ -171,37 +160,6 @@ describe('Repository table component', () => {
expect(findFileTable().props('hasMore')).toBe(limitReached);
});
-
- it.each`
- fetchCounter | pageSize
- ${0} | ${10}
- ${2} | ${30}
- ${4} | ${50}
- ${6} | ${70}
- ${8} | ${90}
- ${10} | ${100}
- ${20} | ${100}
- ${100} | ${100}
- ${200} | ${100}
- `('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
- factory('/');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ fetchCounter });
-
- vm.vm.fetchFiles();
-
- expect($apollo.query).toHaveBeenCalledWith({
- query: paginatedTreeQuery,
- variables: {
- pageSize,
- nextPageCursor: '',
- path: '/',
- projectPath: '',
- ref: '',
- },
- });
- });
});
describe('commit data', () => {
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 9de0666f27a..319321cfcb4 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -4,13 +4,13 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: () => '/new_upload',
@@ -53,14 +53,9 @@ describe('UploadBlobModal', () => {
const findBranchName = () => wrapper.findComponent(GlFormInput);
const findMrToggle = () => wrapper.findComponent(GlToggle);
const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
- const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled;
- const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
- const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+ const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes.disabled;
+ const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes.disabled;
+ const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes.loading;
describe.each`
canPushCode | displayBranchName | displayForkedBranchMessage
@@ -110,9 +105,7 @@ describe('UploadBlobModal', () => {
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ target: 'Not main' });
+ createComponent({ targetBranch: 'Not main' });
await nextTick();
@@ -123,12 +116,10 @@ describe('UploadBlobModal', () => {
describe('completed form', () => {
beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- file: { type: 'jpg' },
- filePreviewURL: 'http://file.com?format=jpg',
- });
+ findUploadDropzone().vm.$emit(
+ 'change',
+ new File(['http://file.com?format=jpg'], 'file.jpg'),
+ );
});
it('enables the upload button when the form is completed', () => {
@@ -184,7 +175,7 @@ describe('UploadBlobModal', () => {
await waitForPromises();
});
- it('creates a flash error', () => {
+ it('creates an alert error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
});
@@ -199,13 +190,6 @@ describe('UploadBlobModal', () => {
);
describe('blob file submission type', () => {
- const submitForm = async () => {
- wrapper.vm.uploadFile = jest.fn();
- wrapper.vm.replaceFile = jest.fn();
- wrapper.vm.submitForm();
- await nextTick();
- };
-
const submitRequest = async () => {
mock = new MockAdapter(axios);
findModal().vm.$emit('primary', mockEvent);
@@ -225,13 +209,6 @@ describe('UploadBlobModal', () => {
expect(findModal().props('actionPrimary').text).toBe('Upload file');
});
- it('calls the default uploadFile when the form submit', async () => {
- await submitForm();
-
- expect(wrapper.vm.uploadFile).toHaveBeenCalled();
- expect(wrapper.vm.replaceFile).not.toHaveBeenCalled();
- });
-
it('makes a POST request', async () => {
await submitRequest();
@@ -261,13 +238,6 @@ describe('UploadBlobModal', () => {
expect(findModal().props('actionPrimary').text).toBe(primaryBtnText);
});
- it('calls the replaceFile when the form submit', async () => {
- await submitForm();
-
- expect(wrapper.vm.replaceFile).toHaveBeenCalled();
- expect(wrapper.vm.uploadFile).not.toHaveBeenCalled();
- });
-
it('makes a PUT request', async () => {
await submitRequest();
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
index 7c48fe440d2..5f872749581 100644
--- a/spec/frontend/repository/mixins/highlight_mixin_spec.js
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -44,8 +44,6 @@ describe('HighlightMixin', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
describe('initHighlightWorker', () => {
const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n');
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 04ffe52bc3f..418a93a10cc 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -126,3 +126,9 @@ export const propsForkInfo = {
aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1',
behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2',
};
+
+export const propsConflictsModal = {
+ sourceDefaultBranch: 'branch-name',
+ sourceName: 'source-name',
+ sourcePath: 'path/to/project',
+};
diff --git a/spec/frontend/repository/pages/blob_spec.js b/spec/frontend/repository/pages/blob_spec.js
index 4fe6188370e..366523e2b8b 100644
--- a/spec/frontend/repository/pages/blob_spec.js
+++ b/spec/frontend/repository/pages/blob_spec.js
@@ -16,10 +16,6 @@ describe('Repository blob page component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a Blob Content Viewer component', () => {
expect(findBlobContentViewer().exists()).toBe(true);
expect(findBlobContentViewer().props('path')).toBe(path);
diff --git a/spec/frontend/repository/pages/index_spec.js b/spec/frontend/repository/pages/index_spec.js
index 559257d414c..e50557e7d61 100644
--- a/spec/frontend/repository/pages/index_spec.js
+++ b/spec/frontend/repository/pages/index_spec.js
@@ -13,8 +13,6 @@ describe('Repository index page component', () => {
}
afterEach(() => {
- wrapper.destroy();
-
updateElementsVisibility.mockClear();
});
diff --git a/spec/frontend/repository/pages/tree_spec.js b/spec/frontend/repository/pages/tree_spec.js
index 36662696c91..b1529d77c7d 100644
--- a/spec/frontend/repository/pages/tree_spec.js
+++ b/spec/frontend/repository/pages/tree_spec.js
@@ -12,8 +12,6 @@ describe('Repository tree page component', () => {
}
afterEach(() => {
- wrapper.destroy();
-
updateElementsVisibility.mockClear();
});
diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
index 3abdfcdaf20..204afc744e7 100644
--- a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
@@ -7,9 +7,39 @@ exports[`Saved replies list item component renders list item 1`] = `
<div
class="gl-display-flex gl-align-items-center"
>
- <strong>
+ <strong
+ data-testid="saved-reply-name"
+ >
test
</strong>
+
+ <div
+ class="gl-ml-auto"
+ >
+ <gl-button-stub
+ aria-label="Edit"
+ buttontextclasses=""
+ category="primary"
+ class="gl-mr-3"
+ data-testid="saved-reply-edit-btn"
+ icon="pencil"
+ size="medium"
+ title="Edit"
+ to="[object Object]"
+ variant="default"
+ />
+
+ <gl-button-stub
+ aria-label="Delete"
+ buttontextclasses=""
+ category="secondary"
+ data-testid="saved-reply-delete-btn"
+ icon="remove"
+ size="medium"
+ title="Delete"
+ variant="danger"
+ />
+ </div>
</div>
<div
@@ -17,5 +47,21 @@ exports[`Saved replies list item component renders list item 1`] = `
>
/assign_reviewer
</div>
+
+ <gl-modal-stub
+ actionprimary="[object Object]"
+ actionsecondary="[object Object]"
+ arialabel=""
+ dismisslabel="Close"
+ modalclass=""
+ modalid="delete-saved-reply-2"
+ size="sm"
+ title="Delete saved reply"
+ titletag="h4"
+ >
+ <gl-sprintf-stub
+ message="Are you sure you want to delete %{name}? This action cannot be undone."
+ />
+ </gl-modal-stub>
</li>
`;
diff --git a/spec/frontend/saved_replies/components/form_spec.js b/spec/frontend/saved_replies/components/form_spec.js
new file mode 100644
index 00000000000..adeda498e6f
--- /dev/null
+++ b/spec/frontend/saved_replies/components/form_spec.js
@@ -0,0 +1,144 @@
+import Vue, { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createdSavedReplyResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply.mutation.graphql.json';
+import createdSavedReplyErrorResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply_with_errors.mutation.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Form from '~/saved_replies/components/form.vue';
+import createSavedReplyMutation from '~/saved_replies/queries/create_saved_reply.mutation.graphql';
+import updateSavedReplyMutation from '~/saved_replies/queries/update_saved_reply.mutation.graphql';
+
+let wrapper;
+let createSavedReplyResponseSpy;
+let updateSavedReplyResponseSpy;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ createSavedReplyResponseSpy = jest.fn().mockResolvedValue(response);
+ updateSavedReplyResponseSpy = jest.fn().mockResolvedValue(response);
+
+ const requestHandlers = [
+ [createSavedReplyMutation, createSavedReplyResponseSpy],
+ [updateSavedReplyMutation, updateSavedReplyResponseSpy],
+ ];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(id = null, response = createdSavedReplyResponse) {
+ const mockApollo = createMockApolloProvider(response);
+
+ return mount(Form, {
+ propsData: {
+ id,
+ },
+ apolloProvider: mockApollo,
+ });
+}
+
+const findSavedReplyNameInput = () => wrapper.find('[data-testid="saved-reply-name-input"]');
+const findSavedReplyNameFormGroup = () =>
+ wrapper.find('[data-testid="saved-reply-name-form-group"]');
+const findSavedReplyContentInput = () => wrapper.find('[data-testid="saved-reply-content-input"]');
+const findSavedReplyContentFormGroup = () =>
+ wrapper.find('[data-testid="saved-reply-content-form-group"]');
+const findSavedReplyFrom = () => wrapper.find('[data-testid="saved-reply-form"]');
+const findAlerts = () => wrapper.findAllComponents(GlAlert);
+const findSubmitBtn = () => wrapper.find('[data-testid="saved-reply-form-submit-btn"]');
+
+describe('Saved replies form component', () => {
+ describe('create saved reply', () => {
+ it('calls apollo mutation', async () => {
+ wrapper = createComponent();
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(createSavedReplyResponseSpy).toHaveBeenCalledWith({
+ id: null,
+ content: 'Test content',
+ name: 'Test',
+ });
+ });
+
+ it('does not submit when form validation fails', async () => {
+ wrapper = createComponent();
+
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(createSavedReplyResponseSpy).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ findFormGroup | findInput | fieldName
+ ${findSavedReplyNameFormGroup} | ${findSavedReplyContentInput} | ${'name'}
+ ${findSavedReplyContentFormGroup} | ${findSavedReplyNameInput} | ${'content'}
+ `('shows errors for empty $fieldName input', async ({ findFormGroup, findInput }) => {
+ wrapper = createComponent(null, createdSavedReplyErrorResponse);
+
+ findInput().setValue('Test');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(findFormGroup().classes('is-invalid')).toBe(true);
+ });
+
+ it('displays errors when mutation fails', async () => {
+ wrapper = createComponent(null, createdSavedReplyErrorResponse);
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ const { errors } = createdSavedReplyErrorResponse;
+ const alertMessages = findAlerts().wrappers.map((x) => x.text());
+
+ expect(alertMessages).toEqual(errors.map((x) => x.message));
+ });
+
+ it('shows loading state when saving', async () => {
+ wrapper = createComponent();
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await nextTick();
+
+ expect(findSubmitBtn().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('updates saved reply', () => {
+ it('calls apollo mutation', async () => {
+ wrapper = createComponent('1');
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(updateSavedReplyResponseSpy).toHaveBeenCalledWith({
+ id: '1',
+ content: 'Test content',
+ name: 'Test',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js
index cad1000473b..f1ecdfecb15 100644
--- a/spec/frontend/saved_replies/components/list_item_spec.js
+++ b/spec/frontend/saved_replies/components/list_item_spec.js
@@ -1,22 +1,50 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createMockDirective } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
import ListItem from '~/saved_replies/components/list_item.vue';
+import deleteSavedReplyMutation from '~/saved_replies/queries/delete_saved_reply.mutation.graphql';
let wrapper;
+let deleteSavedReplyMutationResponse;
function createComponent(propsData = {}) {
+ Vue.use(VueApollo);
+
+ deleteSavedReplyMutationResponse = jest
+ .fn()
+ .mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
+
return shallowMount(ListItem, {
propsData,
+ directives: {
+ GlModal: createMockDirective('gl-modal'),
+ },
+ apolloProvider: createMockApollo([
+ [deleteSavedReplyMutation, deleteSavedReplyMutationResponse],
+ ]),
});
}
describe('Saved replies list item component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders list item', async () => {
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
expect(wrapper.element).toMatchSnapshot();
});
+
+ describe('delete button', () => {
+ it('calls Apollo mutate', async () => {
+ wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer', id: 1 } });
+
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 });
+ });
+ });
});
diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js
index 66e9ddfe148..bc69cb852e0 100644
--- a/spec/frontend/saved_replies/components/list_spec.js
+++ b/spec/frontend/saved_replies/components/list_spec.js
@@ -1,61 +1,39 @@
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
-import VueApollo from 'vue-apollo';
import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json';
import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import List from '~/saved_replies/components/list.vue';
import ListItem from '~/saved_replies/components/list_item.vue';
-import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
let wrapper;
-function createMockApolloProvider(response) {
- Vue.use(VueApollo);
-
- const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
-
- return createMockApollo(requestHandlers);
-}
-
-function createComponent(options = {}) {
- const { mockApollo } = options;
+function createComponent(res = {}) {
+ const { savedReplies } = res.data.currentUser;
return mount(List, {
- apolloProvider: mockApollo,
+ propsData: {
+ savedReplies: savedReplies.nodes,
+ pageInfo: savedReplies.pageInfo,
+ count: savedReplies.count,
+ },
});
}
describe('Saved replies list component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('does not render any list items when response is empty', async () => {
- const mockApollo = createMockApolloProvider(noSavedRepliesResponse);
- wrapper = createComponent({ mockApollo });
-
- await waitForPromises();
+ it('does not render any list items when response is empty', () => {
+ wrapper = createComponent(noSavedRepliesResponse);
expect(wrapper.findAllComponents(ListItem).length).toBe(0);
});
- it('render saved replies count', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
- wrapper = createComponent({ mockApollo });
-
- await waitForPromises();
+ it('render saved replies count', () => {
+ wrapper = createComponent(savedRepliesResponse);
expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)');
});
- it('renders list of saved replies', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ it('renders list of saved replies', () => {
const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
- wrapper = createComponent({ mockApollo });
-
- await waitForPromises();
+ wrapper = createComponent(savedRepliesResponse);
expect(wrapper.findAllComponents(ListItem).length).toBe(2);
expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
diff --git a/spec/frontend/saved_replies/pages/index_spec.js b/spec/frontend/saved_replies/pages/index_spec.js
new file mode 100644
index 00000000000..771025d64ec
--- /dev/null
+++ b/spec/frontend/saved_replies/pages/index_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IndexPage from '~/saved_replies/pages/index.vue';
+import ListItem from '~/saved_replies/components/list_item.vue';
+import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
+
+let wrapper;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mount(IndexPage, {
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Saved replies index page component', () => {
+ it('renders list of saved replies', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(2);
+ expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[0]),
+ );
+ expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[1]),
+ );
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index fb9c0a93907..0aa4f0e1c84 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -653,3 +653,10 @@ export const TEST_FILTER_DATA = {
JSON: { label: 'JSON', value: 'JSON', count: 15 },
},
};
+
+export const SMALL_MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: TEST_RAW_BUCKETS,
+ },
+];
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 83302b90233..8a35ae96d5e 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -17,6 +17,10 @@ describe('GlobalSearchSidebar', () => {
resetQuery: jest.fn(),
};
+ const getterSpies = {
+ currentScope: jest.fn(() => 'issues'),
+ };
+
const createComponent = (initialState, featureFlags) => {
const store = new Vuex.Store({
state: {
@@ -24,6 +28,7 @@ describe('GlobalSearchSidebar', () => {
...initialState,
},
actions: actionSpies,
+ getters: getterSpies,
});
wrapper = shallowMount(GlobalSearchSidebar, {
@@ -52,22 +57,23 @@ describe('GlobalSearchSidebar', () => {
});
describe.each`
- scope | showFilters | ShowsLanguage
+ scope | showFilters | showsLanguage
${'issues'} | ${true} | ${false}
${'merge_requests'} | ${true} | ${false}
${'projects'} | ${false} | ${false}
${'blobs'} | ${false} | ${true}
- `('sidebar scope: $scope', ({ scope, showFilters, ShowsLanguage }) => {
+ `('sidebar scope: $scope', ({ scope, showFilters, showsLanguage }) => {
beforeEach(() => {
- createComponent({ urlQuery: { scope } }, { searchBlobsLanguageAggregation: true });
+ getterSpies.currentScope = jest.fn(() => scope);
+ createComponent({ urlQuery: { scope } });
});
it(`${!showFilters ? "doesn't" : ''} shows filters`, () => {
expect(findFilters().exists()).toBe(showFilters);
});
- it(`${!ShowsLanguage ? "doesn't" : ''} shows language filters`, () => {
- expect(findLanguageAggregation().exists()).toBe(ShowsLanguage);
+ it(`${!showsLanguage ? "doesn't" : ''} shows language filters`, () => {
+ expect(findLanguageAggregation().exists()).toBe(showsLanguage);
});
});
@@ -80,22 +86,4 @@ describe('GlobalSearchSidebar', () => {
});
});
});
-
- describe('when search_blobs_language_aggregation is enabled', () => {
- beforeEach(() => {
- createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: true });
- });
- it('shows the language filter', () => {
- expect(findLanguageAggregation().exists()).toBe(true);
- });
- });
-
- describe('when search_blobs_language_aggregation is disabled', () => {
- beforeEach(() => {
- createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: false });
- });
- it('hides the language filter', () => {
- expect(findLanguageAggregation().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
index 82017754b23..f7b35c7bb14 100644
--- a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
@@ -17,8 +17,12 @@ describe('CheckboxFilter', () => {
setQuery: jest.fn(),
};
+ const getterSpies = {
+ queryLanguageFilters: jest.fn(() => []),
+ };
+
const defaultProps = {
- filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ filtersData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
};
const createComponent = () => {
@@ -27,6 +31,7 @@ describe('CheckboxFilter', () => {
query: MOCK_QUERY,
},
actions: actionSpies,
+ getters: getterSpies,
});
wrapper = shallowMountExtended(CheckboxFilter, {
@@ -73,7 +78,7 @@ describe('CheckboxFilter', () => {
describe('actions', () => {
it('triggers setQuery', () => {
const filter =
- defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value;
+ defaultProps.filtersData.filters[Object.keys(defaultProps.filtersData.filters)[0]].value;
findFormCheckboxGroup().vm.$emit('input', filter);
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js
index 7e564bfa005..51c7bdd9609 100644
--- a/spec/frontend/search/sidebar/components/filters_spec.js
+++ b/spec/frontend/search/sidebar/components/filters_spec.js
@@ -17,6 +17,10 @@ describe('GlobalSearchSidebarFilters', () => {
resetQuery: jest.fn(),
};
+ const defaultGetters = {
+ currentScope: () => 'issues',
+ };
+
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
@@ -24,6 +28,7 @@ describe('GlobalSearchSidebarFilters', () => {
...initialState,
},
actions: actionSpies,
+ getters: defaultGetters,
});
wrapper = shallowMount(ResultsFilters, {
@@ -31,10 +36,6 @@ describe('GlobalSearchSidebarFilters', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSidebarForm = () => wrapper.find('form');
const findStatusFilter = () => wrapper.findComponent(StatusFilter);
const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
@@ -142,7 +143,11 @@ describe('GlobalSearchSidebarFilters', () => {
${'blobs'} | ${false}
`(`ConfidentialityFilter`, ({ scope, showFilter }) => {
beforeEach(() => {
- createComponent({ urlQuery: { scope } });
+ defaultGetters.currentScope = () => scope;
+ createComponent();
+ });
+ afterEach(() => {
+ defaultGetters.currentScope = () => 'issues';
});
it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
@@ -162,7 +167,11 @@ describe('GlobalSearchSidebarFilters', () => {
${'blobs'} | ${false}
`(`StatusFilter`, ({ scope, showFilter }) => {
beforeEach(() => {
- createComponent({ urlQuery: { scope } });
+ defaultGetters.currentScope = () => scope;
+ createComponent();
+ });
+ afterEach(() => {
+ defaultGetters.currentScope = () => 'issues';
});
it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
diff --git a/spec/frontend/search/sidebar/components/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filter_spec.js
index e297d1c33b0..17656ba749b 100644
--- a/spec/frontend/search/sidebar/components/language_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/language_filter_spec.js
@@ -22,7 +22,8 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
};
const getterSpies = {
- langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ languageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ queryLanguageFilters: jest.fn(() => []),
};
const createComponent = (initialState) => {
@@ -48,6 +49,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter);
const findApplyButton = () => wrapper.findByTestId('apply-button');
+ const findResetButton = () => wrapper.findByTestId('reset-button');
const findShowMoreButton = () => wrapper.findByTestId('show-more-button');
const findAlert = () => wrapper.findComponent(GlAlert);
const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
@@ -84,6 +86,25 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
});
});
+ describe('resetButton', () => {
+ describe.each`
+ description | sidebarDirty | queryFilters | isDisabled
+ ${'sidebar dirty only'} | ${true} | ${[]} | ${undefined}
+ ${'query filters only'} | ${false} | ${['JSON', 'C']} | ${undefined}
+ ${'sidebar dirty and query filters'} | ${true} | ${['JSON', 'C']} | ${undefined}
+ ${'no sidebar and no query filters'} | ${false} | ${[]} | ${'true'}
+ `('$description', ({ sidebarDirty, queryFilters, isDisabled }) => {
+ beforeEach(() => {
+ getterSpies.queryLanguageFilters = jest.fn(() => queryFilters);
+ createComponent({ sidebarDirty, query: { ...MOCK_QUERY, language: queryFilters } });
+ });
+
+ it(`button is ${isDisabled ? 'enabled' : 'disabled'}`, () => {
+ expect(findResetButton().attributes('disabled')).toBe(isDisabled);
+ });
+ });
+ });
+
describe('ApplyButton', () => {
describe('when sidebarDirty is false', () => {
beforeEach(() => {
@@ -135,8 +156,8 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
createComponent({});
});
- it('uses getter langugageAggregationBuckets', () => {
- expect(getterSpies.langugageAggregationBuckets).toHaveBeenCalled();
+ it('uses getter languageAggregationBuckets', () => {
+ expect(getterSpies.languageAggregationBuckets).toHaveBeenCalled();
});
it('uses action fetchLanguageAggregation', () => {
diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js
index 94d529348a9..47235b828c3 100644
--- a/spec/frontend/search/sidebar/components/radio_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js
@@ -16,6 +16,10 @@ describe('RadioFilter', () => {
setQuery: jest.fn(),
};
+ const defaultGetters = {
+ currentScope: jest.fn(() => 'issues'),
+ };
+
const defaultProps = {
filterData: stateFilterData,
};
@@ -27,6 +31,7 @@ describe('RadioFilter', () => {
...initialState,
},
actions: actionSpies,
+ getters: defaultGetters,
});
wrapper = shallowMount(RadioFilter, {
@@ -38,11 +43,6 @@ describe('RadioFilter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlRadioButtonGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findGlRadioButtons = () => findGlRadioButtonGroup().findAllComponents(GlFormRadio);
const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map((w) => w.text());
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
index 23c158239dc..3b2d528e1d7 100644
--- a/spec/frontend/search/sidebar/components/scope_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -14,6 +14,10 @@ describe('ScopeNavigation', () => {
fetchSidebarCount: jest.fn(),
};
+ const getterSpies = {
+ currentScope: jest.fn(() => 'issues'),
+ };
+
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
@@ -22,6 +26,7 @@ describe('ScopeNavigation', () => {
...initialState,
},
actions: actionSpies,
+ getters: getterSpies,
});
wrapper = shallowMount(ScopeNavigation, {
@@ -29,16 +34,12 @@ describe('ScopeNavigation', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findNavElement = () => wrapper.find('nav');
const findGlNav = () => wrapper.findComponent(GlNav);
const findGlNavItems = () => wrapper.findAllComponents(GlNavItem);
- const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active'));
- const findGlNavItemActiveLabel = () => findGlNavItemActive().at(0).findAll('span').at(0).text();
- const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1);
+ const findGlNavItemActive = () => wrapper.find('[active=true]');
+ const findGlNavItemActiveLabel = () => findGlNavItemActive().find('[data-testid="label"]');
+ const findGlNavItemActiveCount = () => findGlNavItemActive().find('[data-testid="count"]');
describe('scope navigation', () => {
beforeEach(() => {
@@ -71,8 +72,8 @@ describe('ScopeNavigation', () => {
});
it('has correct active item', () => {
- expect(findGlNavItemActive()).toHaveLength(1);
- expect(findGlNavItemActiveLabel()).toBe('Issues');
+ expect(findGlNavItemActive().exists()).toBe(true);
+ expect(findGlNavItemActiveLabel().text()).toBe('Issues');
});
it('has correct active item count', () => {
@@ -80,7 +81,7 @@ describe('ScopeNavigation', () => {
});
it('does not have plus sign after count text', () => {
- expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(false);
+ expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(false);
});
it('has count is highlighted correctly', () => {
@@ -90,14 +91,26 @@ describe('ScopeNavigation', () => {
describe('scope navigation sets proper state with NO url scope set', () => {
beforeEach(() => {
+ getterSpies.currentScope = jest.fn(() => 'projects');
createComponent({
urlQuery: {},
+ navigation: {
+ ...MOCK_NAVIGATION,
+ projects: {
+ ...MOCK_NAVIGATION.projects,
+ active: true,
+ },
+ issues: {
+ ...MOCK_NAVIGATION.issues,
+ active: false,
+ },
+ },
});
});
it('has correct active item', () => {
- expect(findGlNavItems().at(0).attributes('active')).toBe('true');
- expect(findGlNavItemActiveLabel()).toBe('Projects');
+ expect(findGlNavItemActive().exists()).toBe(true);
+ expect(findGlNavItemActiveLabel().text()).toBe('Projects');
});
it('has correct active item count', () => {
@@ -105,7 +118,7 @@ describe('ScopeNavigation', () => {
});
it('has correct active item count and over limit sign', () => {
- expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(true);
+ expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
index a566b9b99d3..322ce1b16ef 100644
--- a/spec/frontend/search/sort/components/app_spec.js
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -38,11 +38,6 @@ describe('GlobalSearchSort', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSortButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const findSortDropdown = () => wrapper.findComponent(GlDropdown);
const findSortDirectionButton = () => wrapper.findComponent(GlButton);
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 2f87802dfe6..0884411df0c 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -33,7 +33,7 @@ import {
MOCK_AGGREGATIONS,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
setUrlParams: jest.fn(),
joinPaths: jest.fn().mockReturnValue(''),
@@ -47,7 +47,7 @@ describe('Global Search Store Actions', () => {
let mock;
let state;
- const flashCallback = (callCount) => {
+ const alertCallback = (callCount) => {
expect(createAlert).toHaveBeenCalledTimes(callCount);
createAlert.mockClear();
};
@@ -63,12 +63,12 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
+ action | axiosMock | type | expectedMutations | alertCallCount
${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
- `(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
+ `(`axios calls`, ({ action, axiosMock, type, expectedMutations, alertCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
@@ -76,7 +76,7 @@ describe('Global Search Store Actions', () => {
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() =>
- flashCallback(flashCallCount),
+ alertCallback(alertCallCount),
);
});
});
@@ -84,12 +84,12 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
+ action | axiosMock | type | expectedMutations | alertCallCount
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
- `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
+ `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, alertCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
@@ -103,7 +103,7 @@ describe('Global Search Store Actions', () => {
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
- flashCallback(flashCallCount);
+ alertCallback(alertCallCount);
});
});
});
@@ -275,7 +275,7 @@ describe('Global Search Store Actions', () => {
describe.each`
action | axiosMock | type | scope | expectedMutations | errorLogs
${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
- ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'error'} | ${'projects'} | ${[]} | ${1}
${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${'issues'} | ${[]} | ${1}
`('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => {
describe(`on ${type}`, () => {
@@ -290,9 +290,9 @@ describe('Global Search Store Actions', () => {
}
});
- it(`should ${expectedMutations.length === 0 ? 'NOT ' : ''}dispatch ${
- expectedMutations.length === 0 ? '' : 'the correct '
- }mutations for ${scope}`, () => {
+ it(`should ${expectedMutations.length === 0 ? 'NOT' : ''} dispatch ${
+ expectedMutations.length === 0 ? '' : 'the correct'
+ } mutations for ${scope}`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
});
@@ -325,4 +325,26 @@ describe('Global Search Store Actions', () => {
});
});
});
+
+ describe('resetLanguageQueryWithRedirect', () => {
+ it('calls visitUrl and setParams with the state.query', () => {
+ return testAction(actions.resetLanguageQueryWithRedirect, null, state, [], [], () => {
+ expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('resetLanguageQuery', () => {
+ it('calls commit SET_QUERY with value []', () => {
+ state = { ...state, query: { ...state.query, language: ['YAML', 'Text', 'Markdown'] } };
+ return testAction(
+ actions.resetLanguageQuery,
+ null,
+ state,
+ [{ type: types.SET_QUERY, payload: { key: 'language', value: [] } }],
+ [],
+ );
+ });
+ });
});
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 818902ee720..0ef0922c4b0 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -1,12 +1,15 @@
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import * as getters from '~/search/store/getters';
import createState from '~/search/store/state';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
MOCK_QUERY,
MOCK_GROUPS,
MOCK_PROJECTS,
MOCK_AGGREGATIONS,
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ TEST_FILTER_DATA,
+ MOCK_NAVIGATION,
} from '../mock_data';
describe('Global Search Store Getters', () => {
@@ -14,37 +17,55 @@ describe('Global Search Store Getters', () => {
beforeEach(() => {
state = createState({ query: MOCK_QUERY });
+ useMockLocationHelper();
});
describe('frequentGroups', () => {
- beforeEach(() => {
- state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS;
- });
-
it('returns the correct data', () => {
+ state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS;
expect(getters.frequentGroups(state)).toStrictEqual(MOCK_GROUPS);
});
});
describe('frequentProjects', () => {
- beforeEach(() => {
- state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS;
- });
-
it('returns the correct data', () => {
+ state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS;
expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS);
});
});
- describe('langugageAggregationBuckets', () => {
- beforeEach(() => {
+ describe('languageAggregationBuckets', () => {
+ it('returns the correct data', () => {
state.aggregations.data = MOCK_AGGREGATIONS;
+ expect(getters.languageAggregationBuckets(state)).toStrictEqual(
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ );
});
+ });
+ describe('queryLanguageFilters', () => {
it('returns the correct data', () => {
- expect(getters.langugageAggregationBuckets(state)).toStrictEqual(
- MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
- );
+ state.query.language = Object.keys(TEST_FILTER_DATA.filters);
+ expect(getters.queryLanguageFilters(state)).toStrictEqual(state.query.language);
+ });
+ });
+
+ describe('currentScope', () => {
+ it('returns the correct scope name', () => {
+ state.navigation = MOCK_NAVIGATION;
+ expect(getters.currentScope(state)).toBe('issues');
+ });
+ });
+
+ describe('currentUrlQueryHasLanguageFilters', () => {
+ it.each`
+ description | lang | result
+ ${'has valid language'} | ${{ language: ['a', 'b'] }} | ${true}
+ ${'has empty lang'} | ${{ language: [] }} | ${false}
+ ${'has no lang'} | ${{}} | ${false}
+ `('$description', ({ lang, result }) => {
+ state.urlQuery = lang;
+ expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result);
});
});
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index 487ed7bfe03..dfe4e801f11 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -7,6 +7,7 @@ import {
isSidebarDirty,
formatSearchResultCount,
getAggregationsUrl,
+ prepareSearchAggregations,
} from '~/search/store/utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
@@ -15,6 +16,9 @@ import {
MOCK_INFLATED_DATA,
FRESH_STORED_DATA,
STALE_STORED_DATA,
+ MOCK_AGGREGATIONS,
+ SMALL_MOCK_AGGREGATIONS,
+ TEST_RAW_BUCKETS,
} from '../mock_data';
const PREV_TIME = new Date().getTime() - 1;
@@ -226,11 +230,14 @@ describe('Global Search Store Utils', () => {
});
describe.each`
- description | currentQuery | urlQuery | isDirty
- ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false}
- ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true}
- ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false}
- ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true}
+ description | currentQuery | urlQuery | isDirty
+ ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false}
+ ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'c'] }} | ${true}
+ ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null, [SIDEBAR_PARAMS[2]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: undefined }} | ${false}
+ ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: [] }} | ${true}
+ ${'language only no url params'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: undefined }} | ${true}
+ ${'language only url params symetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false}
+ ${'language only url params asymetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${true}
`('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => {
describe(`with ${description} sidebar query data`, () => {
let res;
@@ -263,4 +270,22 @@ describe('Global Search Store Utils', () => {
expect(getAggregationsUrl()).toStrictEqual(`${testURL}search/aggregations`);
});
});
+
+ const TEST_LANGUAGE_QUERY = ['Markdown', 'JSON'];
+ const TEST_EXPECTED_ORDERED_BUCKETS = [
+ TEST_RAW_BUCKETS.find((x) => x.key === 'Markdown'),
+ TEST_RAW_BUCKETS.find((x) => x.key === 'JSON'),
+ ...TEST_RAW_BUCKETS.filter((x) => !TEST_LANGUAGE_QUERY.includes(x.key)),
+ ];
+
+ describe('prepareSearchAggregations', () => {
+ it.each`
+ description | query | data | result
+ ${'has no query'} | ${undefined} | ${MOCK_AGGREGATIONS} | ${MOCK_AGGREGATIONS}
+ ${'has query'} | ${{ language: TEST_LANGUAGE_QUERY }} | ${SMALL_MOCK_AGGREGATIONS} | ${[{ ...SMALL_MOCK_AGGREGATIONS[0], buckets: TEST_EXPECTED_ORDERED_BUCKETS }]}
+ ${'has bad query'} | ${{ language: ['sdf', 'wrt'] }} | ${SMALL_MOCK_AGGREGATIONS} | ${SMALL_MOCK_AGGREGATIONS}
+ `('$description', ({ query, data, result }) => {
+ expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result);
+ });
+ });
});
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index 3975887cfff..423ec6ff63b 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -36,10 +36,6 @@ describe('GlobalSearchTopbar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findGroupFilter = () => wrapper.findComponent(GroupFilter);
const findProjectFilter = () => wrapper.findComponent(ProjectFilter);
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index b2d0297fdc2..78d9efbd686 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -49,10 +49,6 @@ describe('GroupFilter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown);
describe('template', () => {
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index 297a536e075..9eda34b1633 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -49,10 +49,6 @@ describe('ProjectFilter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown);
describe('template', () => {
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
index e51fe9a4cf9..c911fe53d40 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
@@ -24,10 +24,6 @@ describe('Global Search Searchable Dropdown Item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
const findDropdownTitle = () => wrapper.findByTestId('item-title');
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index de1cefa9e9d..5e5f46ff34e 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
@@ -40,10 +40,6 @@ describe('Global Search Searchable Dropdown', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
@@ -133,9 +129,7 @@ describe('Global Search Searchable Dropdown', () => {
describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => {
beforeEach(() => {
createComponent({}, { frequentItems });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchText });
+ findGlDropdownSearch().vm.$emit('input', searchText);
});
it(`should${length ? '' : ' not'} render frequent dropdown items`, () => {
@@ -191,28 +185,33 @@ describe('Global Search Searchable Dropdown', () => {
});
describe('opening the dropdown', () => {
- describe('for the first time', () => {
- beforeEach(() => {
- findGlDropdown().vm.$emit('show');
- });
+ beforeEach(() => {
+ findGlDropdown().vm.$emit('show');
+ });
- it('$emits @search and @first-open', () => {
- expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
- expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
- });
+ it('$emits @search and @first-open on the first open', async () => {
+ expect(wrapper.emitted('search')[0]).toStrictEqual(['']);
+ expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
});
- describe('not for the first time', () => {
- beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hasBeenOpened: true });
- findGlDropdown().vm.$emit('show');
+ describe('when the dropdown has been opened', () => {
+ it('$emits @search with the searchText', async () => {
+ const searchText = 'foo';
+
+ findGlDropdownSearch().vm.$emit('input', searchText);
+ await nextTick();
+
+ expect(wrapper.emitted('search')[1]).toStrictEqual([searchText]);
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
});
- it('$emits @search and not @first-open', () => {
- expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
- expect(wrapper.emitted('first-open')).toBeUndefined();
+ it('does not emit @first-open again', async () => {
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
+
+ findGlDropdownSearch().vm.$emit('input');
+ await nextTick();
+
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index a3098fb81ea..dfb31eeda78 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -119,7 +119,6 @@ describe('Search autocomplete dropdown', () => {
afterEach(() => {
// Undo what we did to the shared <body>
removeBodyAttributes();
- window.gon = {};
resetHTMLFixture();
});
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index 3f856968db6..fe761049a70 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -79,10 +79,6 @@ describe('search_settings/components/search_settings.vue', () => {
buildWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('hides sections that do not match the search term', () => {
const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`);
search(SEARCH_TERM);
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index ddefda2ffc3..0ca350f9ed7 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -26,6 +26,8 @@ import {
REPORT_TYPE_LICENSE_COMPLIANCE,
REPORT_TYPE_SAST,
} from '~/vue_shared/security_reports/constants';
+import { USER_FACING_ERROR_MESSAGE_PREFIX } from '~/lib/utils/error_message';
+import { manageViaMRErrorMessage } from '../constants';
const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
@@ -129,10 +131,6 @@ describe('App component', () => {
const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert);
const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('basic structure', () => {
beforeEach(() => {
createComponent();
@@ -141,7 +139,7 @@ describe('App component', () => {
it('renders main-heading with correct text', () => {
const mainHeading = findMainHeading();
expect(mainHeading.exists()).toBe(true);
- expect(mainHeading.text()).toContain('Security Configuration');
+ expect(mainHeading.text()).toContain('Security configuration');
});
describe('tabs', () => {
@@ -204,18 +202,21 @@ describe('App component', () => {
});
});
- describe('when error occurs', () => {
+ describe('when user facing error occurs', () => {
it('should show Alert with error Message', async () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
- findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+ // Prefixed with USER_FACING_ERROR_MESSAGE_PREFIX as used in lib/gitlab/utils/error_message.rb to indicate a user facing error
+ findFeatureCards()
+ .at(1)
+ .vm.$emit('error', `${USER_FACING_ERROR_MESSAGE_PREFIX} ${manageViaMRErrorMessage}`);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
- expect(findManageViaMRErrorAlert().text()).toEqual('There was a manage via MR error');
+ expect(findManageViaMRErrorAlert().text()).toEqual(manageViaMRErrorMessage);
});
it('should hide Alert when it is dismissed', async () => {
- findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+ findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
@@ -225,6 +226,17 @@ describe('App component', () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
});
});
+
+ describe('when non-user facing error occurs', () => {
+ it('should show Alert with generic error Message', async () => {
+ expect(findManageViaMRErrorAlert().exists()).toBe(false);
+ findFeatureCards().at(1).vm.$emit('error', manageViaMRErrorMessage);
+
+ await nextTick();
+ expect(findManageViaMRErrorAlert().exists()).toBe(true);
+ expect(findManageViaMRErrorAlert().text()).toEqual(i18n.genericErrorText);
+ });
+ });
});
describe('Auto DevOps hint alert', () => {
diff --git a/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
index 467ae35408c..df1fa1a8084 100644
--- a/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
+++ b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
@@ -23,10 +23,6 @@ describe('AutoDevopsAlert component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains correct body text', () => {
expect(wrapper.text()).toContain('Quickly enable all');
});
diff --git a/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js
index 778fea2896a..22f45a92f70 100644
--- a/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js
+++ b/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js
@@ -21,10 +21,6 @@ describe('AutoDevopsEnabledAlert component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains correct body text', () => {
expect(wrapper.text()).toMatchInterpolatedText(AutoDevopsEnabledAlert.i18n.body);
});
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index d10722be8ea..23edd8a69de 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -5,6 +5,7 @@ import FeatureCard from '~/security_configuration/components/feature_card.vue';
import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
+import { manageViaMRErrorMessage } from '../constants';
import { makeFeature } from './utils';
describe('FeatureCard component', () => {
@@ -78,7 +79,6 @@ describe('FeatureCard component', () => {
};
afterEach(() => {
- wrapper.destroy();
feature = undefined;
});
@@ -107,8 +107,8 @@ describe('FeatureCard component', () => {
});
it('should catch and emit manage-via-mr-error', () => {
- findManageViaMr().vm.$emit('error', 'There was a manage via MR error');
- expect(wrapper.emitted('error')).toEqual([['There was a manage via MR error']]);
+ findManageViaMr().vm.$emit('error', manageViaMRErrorMessage);
+ expect(wrapper.emitted('error')).toEqual([[manageViaMRErrorMessage]]);
});
});
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 8f2b5383191..1f8f306c931 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -106,7 +106,7 @@ describe('TrainingProviderList component', () => {
projectFullPath: testProjectPath,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
securityTrainingEnabled: true,
@@ -132,7 +132,6 @@ describe('TrainingProviderList component', () => {
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', testProviderIds[0]);
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
});
diff --git a/spec/frontend/security_configuration/components/upgrade_banner_spec.js b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
index c34d8e47a6c..97087877224 100644
--- a/spec/frontend/security_configuration/components/upgrade_banner_spec.js
+++ b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
@@ -44,7 +44,6 @@ describe('UpgradeBanner component', () => {
});
afterEach(() => {
- wrapper.destroy();
unmockTracking();
});
diff --git a/spec/frontend/security_configuration/constants.js b/spec/frontend/security_configuration/constants.js
new file mode 100644
index 00000000000..d31036a2534
--- /dev/null
+++ b/spec/frontend/security_configuration/constants.js
@@ -0,0 +1 @@
+export const manageViaMRErrorMessage = 'There was a manage via MR error';
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
index efe3f7e8dbf..c278bb4579f 100644
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
@@ -40,6 +40,41 @@ exports[`self-monitor component When the self-monitor project has not been creat
</p>
</div>
+ <gl-alert-stub
+ class="gl-mb-3"
+ dismissible="true"
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ showicon="true"
+ title="Deprecation notice"
+ variant="danger"
+ >
+ <div>
+ Self-monitoring was
+ <a
+ href="/help/update/deprecations.md#gitlab-self-monitoring-project"
+ >
+ deprecated
+ </a>
+ in GitLab 14.9, and is
+ <a
+ href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909"
+ >
+ scheduled for removal
+ </a>
+ in GitLab 16.0. For information on a possible replacement,
+ <a
+ href="https://gitlab.com/groups/gitlab-org/-/epics/6976"
+ >
+ learn more about Opstrace
+ </a>
+ .
+ </div>
+ </gl-alert-stub>
+
<div
class="settings-content"
>
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
index 2dd528a8a1c..83195e9d306 100644
--- a/spec/frontend/sentry/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -4,8 +4,6 @@ import LegacySentryConfig from '~/sentry/legacy_sentry_config';
import SentryConfig from '~/sentry/sentry_config';
describe('Sentry init', () => {
- let originalGon;
-
const dsn = 'https://123@sentry.gitlab.test/123';
const environment = 'test';
const currentUserId = '1';
@@ -14,7 +12,6 @@ describe('Sentry init', () => {
const featureCategory = 'my_feature_category';
beforeEach(() => {
- originalGon = window.gon;
window.gon = {
sentry_dsn: dsn,
sentry_environment: environment,
@@ -28,10 +25,6 @@ describe('Sentry init', () => {
jest.spyOn(SentryConfig, 'init').mockImplementation();
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('exports new version of Sentry in the global object', () => {
// eslint-disable-next-line no-underscore-dangle
expect(window._Sentry.SDK_VERSION).not.toMatch(/^5\./);
diff --git a/spec/frontend/sentry/legacy_index_spec.js b/spec/frontend/sentry/legacy_index_spec.js
index 5c336f8392e..493b4dfde67 100644
--- a/spec/frontend/sentry/legacy_index_spec.js
+++ b/spec/frontend/sentry/legacy_index_spec.js
@@ -4,8 +4,6 @@ import LegacySentryConfig from '~/sentry/legacy_sentry_config';
import SentryConfig from '~/sentry/sentry_config';
describe('Sentry init', () => {
- let originalGon;
-
const dsn = 'https://123@sentry.gitlab.test/123';
const environment = 'test';
const currentUserId = '1';
@@ -14,7 +12,6 @@ describe('Sentry init', () => {
const featureCategory = 'my_feature_category';
beforeEach(() => {
- originalGon = window.gon;
window.gon = {
sentry_dsn: dsn,
sentry_environment: environment,
@@ -28,10 +25,6 @@ describe('Sentry init', () => {
jest.spyOn(SentryConfig, 'init').mockImplementation();
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('exports legacy version of Sentry in the global object', () => {
// eslint-disable-next-line no-underscore-dangle
expect(window._Sentry.SDK_VERSION).toMatch(/^5\./);
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 44acbee9b38..34c5221ef0d 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -1,5 +1,4 @@
import * as Sentry from 'sentrybrowser7';
-import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from '~/sentry/constants';
import SentryConfig from '~/sentry/sentry_config';
@@ -62,11 +61,8 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: SAMPLE_RATE,
allowUrls: options.allowUrls,
environment: options.environment,
- ignoreErrors: IGNORE_ERRORS,
- denyUrls: DENY_URLS,
});
});
@@ -82,11 +78,8 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: SAMPLE_RATE,
allowUrls: options.allowUrls,
environment: 'development',
- ignoreErrors: IGNORE_ERRORS,
- denyUrls: DENY_URLS,
});
});
});
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 85cd8d51272..b5bf739b35a 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
@@ -5,13 +5,13 @@ import { useFakeDate } from 'helpers/fake_date';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import stubChildren from 'helpers/stub_children';
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');
+jest.mock('~/alert');
describe('SetStatusModalWrapper', () => {
let wrapper;
@@ -72,7 +72,6 @@ describe('SetStatusModalWrapper', () => {
};
afterEach(() => {
- wrapper.destroy();
clearEmojiMock();
});
@@ -244,7 +243,7 @@ describe('SetStatusModalWrapper', () => {
return initModal({ mockOnUpdateFailure: false });
});
- it('flashes an error message', async () => {
+ it('alerts an error message', async () => {
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
index a4a2a86dc73..a6ad90123b7 100644
--- 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
@@ -37,10 +37,6 @@ describe('UserProfileSetStatusWrapper', () => {
const findInput = (name) => wrapper.find(`[name="${name}"]`);
const findSetStatusForm = () => wrapper.findComponent(SetStatusForm);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders `SetStatusForm` component and passes expected props', () => {
createComponent();
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 60edab8766a..81b65f4f050 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -30,10 +30,6 @@ describe('AssigneeAvatarLink component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTooltipText = () => wrapper.attributes('title');
const findUserLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
index 7df37d11987..b6b3dbd5b6b 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
@@ -7,7 +7,6 @@ const TEST_AVATAR = `${TEST_HOST}/avatar.png`;
const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`;
describe('AssigneeAvatar', () => {
- let origGon;
let wrapper;
function createComponent(props = {}) {
@@ -24,15 +23,9 @@ describe('AssigneeAvatar', () => {
}
beforeEach(() => {
- origGon = window.gon;
window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL };
});
- afterEach(() => {
- window.gon = origGon;
- wrapper.destroy();
- });
-
const findImg = () => wrapper.find('img');
it('does not show warning icon if assignee can merge', () => {
diff --git a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
index 14a6bdbf907..d561c761c99 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
@@ -17,11 +17,6 @@ describe('AssigneeTitle component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('assignee title', () => {
it('renders assignee', () => {
wrapper = createComponent({
diff --git a/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
index 080171fb2ea..0501c1bae23 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
@@ -49,7 +49,6 @@ describe('Assignees Realtime', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
SidebarMediator.singleton = null;
});
diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index d422292ed9e..1661e28abd2 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -25,10 +25,6 @@ describe('Assignee component', () => {
const findComponentTextNoUsers = () => wrapper.find('[data-testid="no-value"]');
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No assignees/users', () => {
it('displays no assignee icon when collapsed', () => {
createWrapper();
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 7e7d4921cfa..40d3d090bb4 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -26,10 +26,6 @@ describe('CollapsedAssigneeList component', () => {
const findAssignees = () => wrapper.findAllComponents(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('title');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No assignees/users', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
index 4db95114b96..851eaedf0bd 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -21,10 +21,6 @@ describe('CollapsedAssignee assignee component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has author name', () => {
createComponent();
diff --git a/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
index 1161fefcc64..82145b82e21 100644
--- a/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
@@ -20,11 +20,6 @@ describe('IssuableAssignees', () => {
const findUncollapsedAssigneeList = () => wrapper.findComponent(UncollapsedAssigneeList);
const findEmptyAssignee = () => wrapper.find('[data-testid="none"]');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when no assignees are present', () => {
it.each`
signedIn | editable | message
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
index 58b174059fa..a189d3656a2 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
@@ -39,9 +39,6 @@ describe('sidebar assignees', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
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 3aca346ff5f..9f7c587ca9d 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -5,8 +5,8 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { createAlert } from '~/alert';
+import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
@@ -17,7 +17,7 @@ import updateIssueAssigneesMutation from '~/sidebar/queries/update_issue_assigne
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const updateIssueAssigneesMutationSuccess = jest
.fn()
@@ -98,10 +98,7 @@ describe('Sidebar assignees widget', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
fakeApollo = null;
- delete gon.current_username;
});
describe('with passed initial assignees', () => {
@@ -397,7 +394,7 @@ describe('Sidebar assignees widget', () => {
});
it('does not render invite members link on non-issue sidebar', async () => {
- createComponent({ props: { issuableType: IssuableType.MergeRequest } });
+ createComponent({ props: { issuableType: TYPE_MERGE_REQUEST } });
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(false);
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
index 6c22d2f687d..27c31ac56c9 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -21,11 +21,6 @@ describe('boards sidebar remove issue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('renders title', () => {
const title = 'Sidebar item title';
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
index b738d931040..501048bf056 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -16,10 +16,6 @@ describe('Sidebar invite members component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when directly inviting members', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
index be0b14fa997..7895274ab6d 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
@@ -1,6 +1,6 @@
import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
const user = {
@@ -32,10 +32,6 @@ describe('Sidebar participant component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not show `Busy` status when user is not busy', () => {
createComponent();
@@ -56,13 +52,13 @@ describe('Sidebar participant component', () => {
describe('when on merge request sidebar', () => {
it('when project member cannot merge', () => {
- createComponent({ issuableType: IssuableType.MergeRequest });
+ createComponent({ issuableType: TYPE_MERGE_REQUEST });
expect(findIcon().exists()).toBe(true);
});
it('when project member can merge', () => {
- createComponent({ issuableType: IssuableType.MergeRequest, canMerge: true });
+ createComponent({ issuableType: TYPE_MERGE_REQUEST, canMerge: true });
expect(findIcon().exists()).toBe(false);
});
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 03c2e1a37a9..c74a714cca4 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -24,10 +24,6 @@ describe('UncollapsedAssigneeList component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findMoreButton = () => wrapper.find('.user-list-more button');
describe('One assignee/user', () => {
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 37c16bc9235..877d7cd61ee 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
@@ -18,10 +18,6 @@ describe('UserNameWithStatus', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('will render the users name', () => {
expect(wrapper.html()).toContain(name);
});
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
index 81354d64a90..4a2b3b30e6d 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
@@ -18,10 +18,6 @@ describe('Sidebar Confidentiality Content', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits `expandSidebar` event on collapsed icon click', () => {
createComponent();
findCollapsedIcon().trigger('click');
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 b27f7c6b4e1..1ca20dad1c6 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -2,11 +2,11 @@ import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import { confidentialityQueries } from '~/sidebar/constants';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Sidebar Confidentiality Form', () => {
let wrapper;
@@ -38,10 +38,6 @@ describe('Sidebar Confidentiality Form', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits a `closeForm` event when Cancel button is clicked', () => {
createComponent();
findCancelButton().vm.$emit('click');
@@ -58,7 +54,7 @@ describe('Sidebar Confidentiality Form', () => {
expect(findConfidentialToggle().props('loading')).toBe(true);
});
- it('creates a flash if mutation is rejected', async () => {
+ it('creates an alert if mutation is rejected', async () => {
createComponent({ mutate: jest.fn().mockRejectedValue('Error!') });
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
@@ -68,7 +64,7 @@ describe('Sidebar Confidentiality Form', () => {
});
});
- it('creates a flash if mutation contains errors', async () => {
+ it('creates an alert if mutation contains errors', async () => {
createComponent({
mutate: jest.fn().mockResolvedValue({
data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } },
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 e486a8e9ec7..39b30307dd7 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import SidebarConfidentialityWidget, {
@@ -14,7 +14,7 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import { issueConfidentialityResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -48,7 +48,6 @@ describe('Sidebar Confidentiality Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -120,7 +119,7 @@ describe('Sidebar Confidentiality Widget', () => {
});
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
confidentialQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/copy/copyable_field_spec.js b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
index 7790d77bc65..03e131aab35 100644
--- a/spec/frontend/sidebar/components/copy/copyable_field_spec.js
+++ b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
@@ -20,10 +20,6 @@ describe('SidebarCopyableField', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
diff --git a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
index c3de076d6aa..2ae80b2c97b 100644
--- a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import SidebarReferenceWidget from '~/sidebar/components/copy/sidebar_reference_widget.vue';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
@@ -39,10 +39,6 @@ describe('Sidebar Reference Widget', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when reference is loading', () => {
it('sets CopyableField `is-loading` prop to `true`', () => {
createComponent({ referenceQueryHandler: jest.fn().mockReturnValue(new Promise(() => {})) });
@@ -52,7 +48,7 @@ describe('Sidebar Reference Widget', () => {
describe.each([
[TYPE_ISSUE, issueReferenceQuery],
- [IssuableType.MergeRequest, mergeRequestReferenceQuery],
+ [TYPE_MERGE_REQUEST, mergeRequestReferenceQuery],
])('when issuableType is %s', (issuableType, referenceQuery) => {
it('sets CopyableField `value` prop to reference value', async () => {
createComponent({
diff --git a/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
index ca43c219d92..546cabd07d3 100644
--- a/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
@@ -3,7 +3,7 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import CrmContacts from '~/sidebar/components/crm_contacts/crm_contacts.vue';
import getIssueCrmContactsQuery from '~/sidebar/queries/get_issue_crm_contacts.query.graphql';
import issueCrmContactsSubscription from '~/sidebar/queries/issue_crm_contacts.subscription.graphql';
@@ -13,7 +13,7 @@ import {
issueCrmContactsUpdateNullResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Issue crm contacts component', () => {
Vue.use(VueApollo);
@@ -39,7 +39,6 @@ describe('Issue crm contacts component', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 67413cffdda..b9c8655d5d8 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
@@ -13,7 +13,7 @@ import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql'
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -22,10 +22,6 @@ describe('Sidebar date Widget', () => {
let fakeApollo;
const date = '2021-04-15';
- window.gon = {
- first_day_of_week: 1,
- };
-
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
@@ -61,8 +57,11 @@ describe('Sidebar date Widget', () => {
});
};
+ beforeEach(() => {
+ window.gon.first_day_of_week = 1;
+ });
+
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -159,7 +158,7 @@ describe('Sidebar date Widget', () => {
expect(wrapper.findComponent(SidebarInheritDate).exists()).toBe(false);
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
index cbe01263dcd..1bb910c53ea 100644
--- a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
@@ -27,10 +27,6 @@ describe('SidebarFormattedDate', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays formatted date', () => {
expect(findFormattedDate().text()).toBe('Apr 15, 2021');
});
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 a7556b9110c..97debe3088d 100644
--- a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
@@ -31,10 +31,6 @@ describe('SidebarInheritDate', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays formatted fixed and inherited dates with radio buttons', () => {
expect(wrapper.findAllComponents(SidebarFormattedDate)).toHaveLength(2);
expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(2);
diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
index 1a78ce4ddee..e356f02a36b 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
@@ -17,10 +17,6 @@ describe('EscalationStatus', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownComponent = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownMenu = () => findDropdownComponent().find('.dropdown-menu');
diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
index 2dded61c073..00b57b4916e 100644
--- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
@@ -18,11 +18,11 @@ import {
} from '~/sidebar/constants';
import waitForPromises from 'helpers/wait_for_promises';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -57,7 +57,7 @@ describe('SidebarEscalationStatus', () => {
canUpdate: true,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
apolloProvider,
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
index 4f2a89e20db..084ca5ed3fc 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
@@ -29,10 +29,6 @@ describe('DropdownButton', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownButton = () => wrapper.findComponent(GlButton);
const findDropdownText = () => wrapper.find('.dropdown-toggle-text');
const findDropdownIcon = () => wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
index 59e95edfa20..bb7554ff21d 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -32,10 +32,6 @@ describe('DropdownContentsCreateView', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('disableCreate', () => {
it('returns `true` when label title and color is not defined', () => {
@@ -174,11 +170,17 @@ describe('DropdownContentsCreateView', () => {
});
await nextTick();
- const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview');
- const colorInputEl = wrapper.find('.color-input-container').findComponent(GlFormInput);
+ const colorPreviewEl = wrapper
+ .find('.color-input-container')
+ .findAllComponents(GlFormInput)
+ .at(0);
+ const colorInputEl = wrapper
+ .find('.color-input-container')
+ .findAllComponents(GlFormInput)
+ .at(1);
expect(colorPreviewEl.exists()).toBe(true);
- expect(colorPreviewEl.attributes('style')).toContain('background-color');
+ expect(colorPreviewEl.attributes('value')).toBe('#ff0000');
expect(colorInputEl.exists()).toBe(true);
expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000');
expect(colorInputEl.attributes('value')).toBe('#ff0000');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
index 865dc8fe8fb..7940518f1e8 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -51,10 +51,6 @@ describe('DropdownContentsLabelsView', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]');
const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]');
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
index e9ffda7c251..0a17c5f8721 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
@@ -28,10 +28,6 @@ describe('DropdownContent', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('dropdownContentsView', () => {
it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
index 6c3fda421ff..367f6007194 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
@@ -31,10 +31,6 @@ describe('DropdownTitle', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders component container element with string "Labels"', () => {
expect(wrapper.text()).toContain('Labels');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
index 56f25a1c6a4..6684cf0c5f4 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -19,15 +19,11 @@ describe('DropdownValueCollapsedComponent', () => {
wrapper = shallowMount(DropdownValueCollapsedComponent, {
propsData: { ...defaultProps, ...props },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlIcon = () => wrapper.findComponent(GlIcon);
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
index a1ccc9d2ab1..70aafceb00c 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
@@ -28,10 +28,6 @@ describe('DropdownValue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns a label filter URL based on provided label param', () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
index e14c0e308ce..468dd14c9ee 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
@@ -26,10 +26,6 @@ describe('LabelItem', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders gl-link component', () => {
expect(wrapper.findComponent(GlLink).exists()).toBe(true);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
index a3b10c18374..806064b2202 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
@@ -40,10 +40,6 @@ describe('LabelsSelectRoot', () => {
store = new Vuex.Store(labelsSelectModule());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('handleVuexActionDispatch', () => {
const touchedLabels = [
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
index 55651bccaa8..c27afb75375 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
@@ -1,14 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('LabelsSelect Actions', () => {
let state;
@@ -100,7 +100,7 @@ describe('LabelsSelect Actions', () => {
);
});
- it('shows flash error', () => {
+ it('shows alert error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
@@ -184,7 +184,7 @@ describe('LabelsSelect Actions', () => {
);
});
- it('shows flash error', () => {
+ it('shows alert error', () => {
actions.receiveCreateLabelFailure({ commit: () => {} });
expect(createAlert).toHaveBeenCalledWith({ message: 'Error creating label.' });
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 79b164b0ea7..4ca0a813da2 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { workspaceLabelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
@@ -15,7 +15,7 @@ import {
workspaceLabelsQueryResponse,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const colors = Object.keys(mockSuggestedColors);
@@ -87,10 +87,6 @@ describe('DropdownContentsCreateView', () => {
gon.suggested_label_colors = mockSuggestedColors;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a palette of 21 colors', () => {
createComponent();
expect(findAllColors()).toHaveLength(21);
@@ -103,7 +99,7 @@ describe('DropdownContentsCreateView', () => {
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
- expect(findSelectedColor().attributes('style')).toBe('background-color: rgb(0, 153, 102);');
+ expect(findSelectedColor().attributes('value')).toBe('#009966');
});
it('shows correct color hex code after selecting a color', async () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
index 913badccbe4..c351a60735b 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -9,7 +9,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
@@ -17,7 +17,7 @@ import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -64,10 +64,6 @@ describe('DropdownContentsLabelsView', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
index 9bbb1413ee9..e9023cb9ff6 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
@@ -66,10 +66,6 @@ describe('DropdownContent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
index 9a6e0ca3ccd..ad1edaa6671 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
@@ -20,10 +20,6 @@ describe('DropdownFooter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
describe('Labels view', () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
index d9001dface4..824f91812fb 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
@@ -28,10 +28,6 @@ describe('DropdownHeader', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findGoBackButton = () => wrapper.findByTestId('go-back-button');
const findDropdownTitle = () => wrapper.findByTestId('dropdown-header-title');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
index 585048983c9..d70b989b493 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
@@ -30,10 +30,6 @@ describe('DropdownValue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there are no labels', () => {
beforeEach(() => {
createComponent(
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
index 4fa65c752f9..715dd4e034e 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
@@ -30,10 +30,6 @@ describe('EmbeddedLabelsList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there are no labels', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
index 74188a77994..377d1894411 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
@@ -19,10 +19,6 @@ describe('LabelItem', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders label color element', () => {
const colorEl = wrapper.find('[data-testid="label-color-box"]');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index fd8e72bac49..3101fd90f2e 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -3,8 +3,8 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
+import { createAlert } from '~/alert';
+import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
@@ -25,7 +25,7 @@ import {
mockRegularLabel,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -36,9 +36,9 @@ const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a proble
const updateLabelsMutation = {
[TYPE_ISSUE]: updateIssueLabelsMutation,
- [IssuableType.MergeRequest]: updateMergeRequestLabelsMutation,
+ [TYPE_MERGE_REQUEST]: updateMergeRequestLabelsMutation,
[TYPE_EPIC]: updateEpicLabelsMutation,
- [IssuableType.TestCase]: updateTestCaseLabelsMutation,
+ [TYPE_TEST_CASE]: updateTestCaseLabelsMutation,
};
describe('LabelsSelectRoot', () => {
@@ -83,10 +83,6 @@ describe('LabelsSelectRoot', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders component with classes `labels-select-wrapper gl-relative`', () => {
createComponent();
expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'gl-relative']);
@@ -150,7 +146,7 @@ describe('LabelsSelectRoot', () => {
});
});
- it('creates flash with error message when query is rejected', async () => {
+ it('creates alert with error message when query is rejected', async () => {
createComponent({ queryHandler: errorQueryHandler });
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
@@ -214,9 +210,9 @@ describe('LabelsSelectRoot', () => {
describe.each`
issuableType
${TYPE_ISSUE}
- ${IssuableType.MergeRequest}
+ ${TYPE_MERGE_REQUEST}
${TYPE_EPIC}
- ${IssuableType.TestCase}
+ ${TYPE_TEST_CASE}
`('when updating labels for $issuableType', ({ issuableType }) => {
const label = { id: 'gid://gitlab/ProjectLabel/2' };
diff --git a/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
index 2abb0c24d7d..ad9efc371f0 100644
--- a/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { createStore as createMrStore } from '~/mr_notes/stores';
import createStore from '~/notes/stores';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
@@ -8,7 +8,7 @@ import eventHub from '~/sidebar/event_hub';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('EditFormButtons', () => {
let wrapper;
@@ -51,11 +51,6 @@ describe('EditFormButtons', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
@@ -128,7 +123,7 @@ describe('EditFormButtons', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
- it('does not flash an error message', () => {
+ it('does not alert an error message', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -161,7 +156,7 @@ describe('EditFormButtons', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
- it('calls flash with the correct message', () => {
+ it('calls alert with the correct message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: `Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
});
diff --git a/spec/frontend/sidebar/components/lock/edit_form_spec.js b/spec/frontend/sidebar/components/lock/edit_form_spec.js
index 4ae9025ee39..06cce7bd7ca 100644
--- a/spec/frontend/sidebar/components/lock/edit_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_spec.js
@@ -24,11 +24,6 @@ describe('Edit Form Dropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index 8f825847cfc..d26ef7298ce 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -62,16 +62,11 @@ describe('IssuableLockForm', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
index b492753867b..8a0db1715f3 100644
--- a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
import { __ } from '~/locale';
import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
@@ -12,7 +12,7 @@ describe('MilestoneDropdown component', () => {
const propsData = {
attrWorkspacePath: 'full/path',
issuableType: TYPE_ISSUE,
- workspaceType: WorkspaceType.project,
+ workspaceType: WORKSPACE_PROJECT,
};
const findHiddenInput = () => wrapper.find('input');
diff --git a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index 72279f44e80..e247f5d27fa 100644
--- a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -63,7 +63,6 @@ describe('IssuableMoveDropdown', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/sidebar/components/move/move_issue_button_spec.js b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
index acd6b23c1f5..eb5e23c6047 100644
--- a/spec/frontend/sidebar/components/move/move_issue_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
@@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { visitUrl } from '~/lib/utils/url_utility';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import ProjectSelect from '~/sidebar/components/move/issuable_move_dropdown.vue';
import MoveIssueButton from '~/sidebar/components/move/move_issue_button.vue';
import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
@@ -118,7 +118,7 @@ describe('MoveIssueButton', () => {
expect(findProjectSelect().props('moveInProgress')).toBe(false);
});
- it('creates a flash and logs errors when a mutation returns errors', async () => {
+ it('creates an alert and logs errors when a mutation returns errors', async () => {
createComponent(resolvedMutationWithErrorsMock);
emitProjectSelectEvent();
diff --git a/spec/frontend/sidebar/components/move/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
index c65bad642a0..662a39c829d 100644
--- a/spec/frontend/sidebar/components/move/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -6,7 +6,7 @@ import { GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
import issuableEventHub from '~/issues/list/eventhub';
@@ -22,7 +22,7 @@ import {
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/logger');
useMockLocationHelper();
@@ -159,7 +159,6 @@ describe('MoveIssuesButton', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -389,7 +388,7 @@ describe('MoveIssuesButton', () => {
});
describe('shows errors', () => {
- it('does not create flashes or logs errors when no issue is selected', async () => {
+ it('does not create alerts or logs errors when no issue is selected', async () => {
createComponent();
emitMoveIssuablesEvent();
@@ -399,7 +398,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when only tasks are selected', async () => {
+ it('does not create alerts or logs errors when only tasks are selected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
emitMoveIssuablesEvent();
@@ -409,7 +408,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when only test cases are selected', async () => {
+ it('does not create alerts or logs errors when only test cases are selected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
emitMoveIssuablesEvent();
@@ -419,7 +418,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
+ it('does not create alerts or logs errors when only tasks and test cases are selected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
emitMoveIssuablesEvent();
@@ -429,7 +428,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when issues are moved without errors', async () => {
+ it('does not create alerts or logs errors when issues are moved without errors', async () => {
createComponent(
{ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
resolvedMutationWithoutErrorsMock,
@@ -442,7 +441,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('creates a flash and logs errors when a mutation returns errors', async () => {
+ it('creates an alert and logs errors when a mutation returns errors', async () => {
createComponent(
{ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
resolvedMutationWithErrorsMock,
@@ -462,14 +461,14 @@ describe('MoveIssuesButton', () => {
`Error moving issue. Error message: ${mockMutationErrorMessage}`,
);
- // Only one flash is created even if multiple errors are reported
+ // Only one alert is created even if multiple errors are reported
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while moving the issues.',
});
});
- it('creates a flash but not logs errors when a mutation is rejected', async () => {
+ it('creates an alert but not logs errors when a mutation is rejected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
emitMoveIssuablesEvent();
diff --git a/spec/frontend/sidebar/components/participants/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js
index f7a626a189c..72d83ebeca4 100644
--- a/spec/frontend/sidebar/components/participants/participants_spec.js
+++ b/spec/frontend/sidebar/components/participants/participants_spec.js
@@ -1,203 +1,114 @@
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import Participants from '~/sidebar/components/participants/participants.vue';
-const PARTICIPANT = {
- id: 1,
- state: 'active',
- username: 'marcene',
- name: 'Allie Will',
- web_url: 'foo.com',
- avatar_url: 'gravatar.com/avatar/xxx',
-};
-
-const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
-
-describe('Participants', () => {
+describe('Participants component', () => {
let wrapper;
- const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]');
- const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
+ const participant = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+ };
- const mountComponent = (propsData) =>
- shallowMount(Participants, {
- propsData,
- });
+ const participants = [participant, { ...participant, id: 2 }, { ...participant, id: 3 }];
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findMoreParticipantsButton = () => wrapper.findComponent(GlButton);
+ const findCollapsedIcon = () => wrapper.find('.sidebar-collapsed-icon');
+ const findParticipantsAuthor = () => wrapper.findAll('.participants-author');
+
+ const mountComponent = (propsData) => shallowMount(Participants, { propsData });
describe('collapsed sidebar state', () => {
it('shows loading spinner when loading', () => {
- wrapper = mountComponent({
- loading: true,
- });
+ wrapper = mountComponent({ loading: true });
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('does not show loading spinner not loading', () => {
- wrapper = mountComponent({
- loading: false,
- });
+ it('does not show loading spinner when not loading', () => {
+ wrapper = mountComponent({ loading: false });
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('shows participant count when given', () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- });
+ wrapper = mountComponent({ participants });
- expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ expect(findCollapsedIcon().text()).toBe(participants.length.toString());
});
it('shows full participant count when there are hidden participants', () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 1,
- });
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 1 });
- expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ expect(findCollapsedIcon().text()).toBe(participants.length.toString());
});
});
describe('expanded sidebar state', () => {
it('shows loading spinner when loading', () => {
- wrapper = mountComponent({
- loading: true,
- });
+ wrapper = mountComponent({ loading: true });
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => {
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
const numberOfLessParticipants = 2;
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: false,
- });
-
- await nextTick();
- expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
+ wrapper = mountComponent({ participants, numberOfLessParticipants });
+
+ expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants);
});
it('when only showing all participants, each has an avatar', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: true,
- });
-
- await nextTick();
- expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
+
+ await findMoreParticipantsButton().vm.$emit('click');
+
+ expect(findParticipantsAuthor()).toHaveLength(participants.length);
});
it('does not have more participants link when they can all be shown', () => {
const numberOfLessParticipants = 100;
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
-
- expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
- expect(getMoreParticipantsButton().exists()).toBe(false);
- });
+ wrapper = mountComponent({ participants, numberOfLessParticipants });
- it('when too many participants, has more participants link to show more', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: false,
- });
-
- await nextTick();
- expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
+ expect(participants.length).toBeLessThan(numberOfLessParticipants);
+ expect(findMoreParticipantsButton().exists()).toBe(false);
});
- it('when too many participants and already showing them, has more participants link to show less', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: true,
- });
-
- await nextTick();
- expect(getMoreParticipantsButton().text()).toBe('- show less');
- });
+ it('when too many participants, has more participants link to show more', () => {
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
- it('clicking more participants link emits event', () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
+ expect(findMoreParticipantsButton().text()).toBe('+ 1 more');
+ });
- expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
+ it('when too many participants and already showing them, has more participants link to show less', async () => {
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
- getMoreParticipantsButton().vm.$emit('click');
+ await findMoreParticipantsButton().vm.$emit('click');
- expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
+ expect(findMoreParticipantsButton().text()).toBe('- show less');
});
- it('clicking on participants icon emits `toggleSidebar` event', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- const spy = jest.spyOn(wrapper.vm, '$emit');
+ it('clicking on participants icon emits `toggleSidebar` event', () => {
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
- wrapper.find('.sidebar-collapsed-icon').trigger('click');
+ findCollapsedIcon().trigger('click');
- await nextTick();
- expect(spy).toHaveBeenCalledWith('toggleSidebar');
- spy.mockRestore();
+ expect(wrapper.emitted('toggleSidebar')).toEqual([[]]);
});
});
describe('when not showing participants label', () => {
beforeEach(() => {
- wrapper = mountComponent({
- participants: PARTICIPANT_LIST,
- showParticipantLabel: false,
- });
+ wrapper = mountComponent({ participants, showParticipantLabel: false });
});
it('does not show sidebar collapsed icon', () => {
- expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false);
+ expect(findCollapsedIcon().exists()).toBe(false);
});
it('does not show participants label title', () => {
diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
index 859e63b3df6..914e848eced 100644
--- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
+++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
@@ -35,7 +35,6 @@ describe('Sidebar Participants Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
index 68ecd62e4c6..0f595ab21a5 100644
--- a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
@@ -16,11 +16,6 @@ describe('ReviewerTitle component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('reviewer title', () => {
it('renders reviewer', () => {
wrapper = createComponent({
diff --git a/spec/frontend/sidebar/components/reviewers/reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
index 229f7ffbe04..016ec9225da 100644
--- a/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
@@ -35,10 +35,6 @@ describe('Reviewer component', () => {
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No reviewers/users', () => {
it('displays no reviewer icon when collapsed', () => {
createWrapper();
diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
index 57ae146a27a..a221d28704b 100644
--- a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
@@ -44,9 +44,6 @@ describe('sidebar reviewers', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
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 d00c8dcb653..483449f924b 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -38,10 +38,6 @@ describe('UncollapsedReviewerList component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('single reviewer', () => {
const user = userDataMock();
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 71c6c259c32..8a27e82093d 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -2,13 +2,14 @@ import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/constants';
+import { createAlert } from '~/alert';
+import { TYPE_INCIDENT } from '~/issues/constants';
+import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import updateIssuableSeverity from '~/sidebar/queries/update_issuable_severity.mutation.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('SidebarSeverity', () => {
let wrapper;
@@ -22,7 +23,7 @@ describe('SidebarSeverity', () => {
const propsData = {
projectPath,
iid,
- issuableType: ISSUABLE_TYPES.INCIDENT,
+ issuableType: TYPE_INCIDENT,
initialSeverity: severity,
...props,
};
@@ -47,10 +48,6 @@ describe('SidebarSeverity', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
const findEditBtn = () => wrapper.findByTestId('edit-button');
const findDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
index 9f3d689edee..7a0044c00ac 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
@@ -10,7 +10,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
@@ -23,7 +23,7 @@ import {
noCurrentMilestoneResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('SidebarDropdown component', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 060a2873e04..53d81e3fcaf 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@@ -27,7 +27,7 @@ import {
mockMilestone2,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('SidebarDropdownWidget', () => {
let wrapper;
@@ -140,7 +140,7 @@ describe('SidebarDropdownWidget', () => {
},
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
SidebarEditableItem,
@@ -157,11 +157,6 @@ describe('SidebarDropdownWidget', () => {
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when not editing', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/sidebar/components/status/status_dropdown_spec.js b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
index 5a75299c3a4..229b51ea568 100644
--- a/spec/frontend/sidebar/components/status/status_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
@@ -14,10 +14,6 @@ describe('SubscriptionsDropdown component', () => {
wrapper = shallowMount(StatusDropdown);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with no value selected', () => {
beforeEach(() => {
createComponent();
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 c94f9918243..7275557e7f2 100644
--- a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
@@ -15,7 +15,7 @@ import {
mergeRequestSubscriptionMutationResponse,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/plugins/global_toast');
Vue.use(VueApollo);
@@ -62,7 +62,6 @@ describe('Sidebar Subscriptions Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -138,7 +137,7 @@ describe('Sidebar Subscriptions Widget', () => {
});
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
subscriptionsQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
index 3fb8214606c..eaf7bc13d20 100644
--- a/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
@@ -15,10 +15,6 @@ describe('SubscriptionsDropdown component', () => {
wrapper = shallowMount(SubscriptionsDropdown);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with no value selected', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
index 1a1aa370eef..cae21189ee0 100644
--- a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
@@ -16,11 +16,6 @@ describe('Subscriptions', () => {
}),
);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('shows loading spinner when loading', () => {
wrapper = mountComponent({
loading: true,
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
index 715f66d305a..a7c3867c359 100644
--- a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -47,8 +47,8 @@ describe('Create Timelog Form', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findDocsLink = () => wrapper.findByTestId('timetracking-docs-link');
const findSaveButton = () => findModal().props('actionPrimary');
- const findSaveButtonLoadingState = () => findSaveButton().attributes[0].loading;
- const findSaveButtonDisabledState = () => findSaveButton().attributes[0].disabled;
+ const findSaveButtonLoadingState = () => findSaveButton().attributes.loading;
+ const findSaveButtonDisabledState = () => findSaveButton().attributes.disabled;
const submitForm = () => findForm().trigger('submit');
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 0259aee48f0..713ae83cbf1 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -6,7 +6,7 @@ import VueApollo from 'vue-apollo';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Report from '~/sidebar/components/time_tracking/report.vue';
import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.graphql';
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
@@ -17,7 +17,7 @@ import {
timelogToRemoveId,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Issuable Time Tracking Report', () => {
Vue.use(VueApollo);
@@ -51,7 +51,6 @@ describe('Issuable Time Tracking Report', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 45d8b5e4647..91c013596d7 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -32,7 +32,7 @@ describe('Issuable Time Tracker', () => {
const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
return mount(TimeTracker, {
propsData: { ...defaultProps, ...props },
- directives: { GlTooltip: createMockDirective() },
+ directives: { GlTooltip: createMockDirective('gl-tooltip') },
stubs: {
transition: stubTransition(),
},
@@ -53,10 +53,6 @@ describe('Issuable Time Tracker', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Initialization', () => {
beforeEach(() => {
wrapper = mountComponent();
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index 5bfe3b59eb3..39b480b295c 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -4,13 +4,13 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import { todosResponse, noTodosResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -41,7 +41,6 @@ describe('Sidebar Todo Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -77,7 +76,7 @@ describe('Sidebar Todo Widget', () => {
});
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
todosQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
index fb07029a249..472a89e9b21 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -22,7 +22,6 @@ describe('Todo Button', () => {
});
afterEach(() => {
- wrapper.destroy();
dispatchEventSpy = null;
jest.clearAllMocks();
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
index 8e6597bf80f..4da915f0dd3 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
@@ -21,10 +21,6 @@ describe('SidebarTodo', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
state | classes
${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
diff --git a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
index cf9b2828dde..0370d5e337d 100644
--- a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
+++ b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
@@ -17,10 +17,6 @@ describe('ToggleSidebar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlButton = () => wrapper.findComponent(GlButton);
it('should render the "chevron-double-lg-left" icon when collapsed', () => {
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 391cbb1e0d5..844320efc1c 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -243,6 +243,7 @@ export const issuableDueDateResponse = (dueDate = null) => ({
__typename: 'Issue',
id: 'gid://gitlab/Issue/4',
dueDate,
+ dueDateFixed: dueDate,
},
},
},
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index 77b1ccb4f9a..f2003aee96e 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -7,7 +7,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/plugins/global_toast');
jest.mock('~/commons/nav/user_merge_requests');
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index fec300ddd7e..7eb0468c5be 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -28,10 +28,13 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
data-uploads-path=""
>
<markdown-header-stub
+ data-testid="markdownHeader"
enablepreview="true"
linecontent=""
+ markdownpreviewpath="foo/"
restrictedtoolbaritems=""
suggestionstartindex="0"
+ uploadspath=""
/>
<div
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index e7dab0ad79d..0d0e78e9179 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -9,7 +9,7 @@ import { stubPerformanceWebAPI } from 'helpers/performance';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
@@ -25,7 +25,7 @@ import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
const TEST_API_ERROR = new Error('TEST_API_ERROR');
@@ -94,7 +94,6 @@ describe('Snippet Edit app', () => {
let mutateSpy;
const relativeUrlRoot = '/foo/';
- const originalRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
stubPerformanceWebAPI();
@@ -108,12 +107,6 @@ describe('Snippet Edit app', () => {
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- gon.relative_url_root = originalRelativeUrlRoot;
- });
-
const findBlobActions = () => wrapper.findComponent(SnippetBlobActionsEdit);
const findCancelButton = () => wrapper.findByTestId('snippet-cancel-btn');
const clickSubmitBtn = () => wrapper.findByTestId('snippet-edit-form').trigger('submit');
@@ -132,10 +125,6 @@ describe('Snippet Edit app', () => {
props = {},
selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING,
} = {}) => {
- if (wrapper) {
- throw new Error('wrapper already created');
- }
-
const requestHandlers = [
[GetSnippetQuery, getSpy],
// See `mutateSpy` declaration comment for why we send a key
@@ -347,7 +336,7 @@ describe('Snippet Edit app', () => {
projectPath
${'project/path'}
${''}
- `('should flash error (projectPath=$projectPath)', async ({ projectPath }) => {
+ `('should alert error (projectPath=$projectPath)', async ({ projectPath }) => {
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('createSnippet'));
await createComponentAndLoad({
@@ -373,7 +362,7 @@ describe('Snippet Edit app', () => {
${'project/path'}
${''}
`(
- 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
+ 'should alert error with (snippet=$snippetGid, projectPath=$projectPath)',
async ({ projectPath }) => {
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('updateSnippet'));
@@ -405,7 +394,7 @@ describe('Snippet Edit app', () => {
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
});
- it('should flash', () => {
+ it('should alert', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_API_ERROR.message}`,
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
index ed5ea6cab8a..d8c6ad3278a 100644
--- a/spec/frontend/snippets/components/embed_dropdown_spec.js
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -17,11 +17,6 @@ describe('snippets/components/embed_dropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSectionsData = () => {
const sections = [];
let current = {};
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index 032dcf8e5f5..45a7c7b0b4a 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -50,10 +50,6 @@ describe('Snippet view app', () => {
stubPerformanceWebAPI();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
expect(findLoadingIcon().exists()).toBe(true);
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 a650353093d..58f47e8b0dc 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -56,11 +56,6 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
const triggerBlobDelete = (idx) => findBlobEdits().at(idx).vm.$emit('delete');
const triggerBlobUpdate = (idx, props) => findBlobEdits().at(idx).vm.$emit('blob-updated', props);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('multi-file snippets rendering', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 82c4a37ccc9..b699e056576 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -4,14 +4,14 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_ID = 'blob_local_7';
const TEST_PATH = 'foo/bar/test.md';
@@ -62,8 +62,6 @@ describe('Snippet Blob Edit component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
axiosMock.restore();
});
@@ -123,7 +121,7 @@ describe('Snippet Blob Edit component', () => {
createComponent();
});
- it('should call flash', async () => {
+ it('should call alert', async () => {
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index c7ff8c21d80..840bca8c9c8 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -62,10 +62,6 @@ describe('Blob Embeddable', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('renders correct components', () => {
createComponent();
diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js
index ff75515e71a..2b42eba19c2 100644
--- a/spec/frontend/snippets/components/snippet_description_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js
@@ -30,10 +30,6 @@ describe('Snippet Description Edit component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/snippets/components/snippet_description_view_spec.js b/spec/frontend/snippets/components/snippet_description_view_spec.js
index 14f116f2aaf..3c5d50ccaa6 100644
--- a/spec/frontend/snippets/components/snippet_description_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_view_spec.js
@@ -17,10 +17,6 @@ describe('Snippet Description component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index c930c9f635b..994cf65c1f5 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
+import { GlModal, GlButton, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
+import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+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 { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
@@ -10,31 +11,41 @@ import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
-import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert';
+import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
+import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
+import { getCanCreateProjectSnippetMock, getCanCreatePersonalSnippetMock } from '../mock_data';
-jest.mock('~/flash');
+const ERROR_MSG = 'Foo bar';
+const ERR = { message: ERROR_MSG };
+
+const MUTATION_TYPES = {
+ RESOLVE: jest.fn().mockResolvedValue({ data: { destroySnippet: { errors: [] } } }),
+ REJECT: jest.fn().mockRejectedValue(ERR),
+};
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
describe('Snippet header component', () => {
let wrapper;
let snippet;
- let mutationTypes;
- let mutationVariables;
let mock;
+ let mockApollo;
- let errorMsg;
- let err;
- const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam';
const canReportSpam = true;
const GlEmoji = { template: '<img/>' };
function createComponent({
- loading = false,
permissions = {},
- mutationRes = mutationTypes.RESOLVE,
snippetProps = {},
provide = {},
+ canCreateProjectSnippetMock = jest.fn().mockResolvedValue(getCanCreateProjectSnippetMock()),
+ canCreatePersonalSnippetMock = jest.fn().mockResolvedValue(getCanCreatePersonalSnippetMock()),
+ deleteSnippetMock = MUTATION_TYPES.RESOLVE,
} = {}) {
const defaultProps = Object.assign(snippet, snippetProps);
if (permissions) {
@@ -42,17 +53,14 @@ describe('Snippet header component', () => {
...permissions,
});
}
- const $apollo = {
- queries: {
- canCreateSnippet: {
- loading,
- },
- },
- mutate: mutationRes,
- };
+
+ mockApollo = createMockApollo([
+ [CanCreateProjectSnippet, canCreateProjectSnippetMock],
+ [CanCreatePersonalSnippet, canCreatePersonalSnippetMock],
+ [DeleteSnippetMutation, deleteSnippetMock],
+ ]);
wrapper = mount(SnippetHeader, {
- mocks: { $apollo },
provide: {
reportAbusePath,
canReportSpam,
@@ -64,9 +72,9 @@ describe('Snippet header component', () => {
},
},
stubs: {
- ApolloMutation,
GlEmoji,
},
+ apolloProvider: mockApollo,
});
}
@@ -91,6 +99,7 @@ describe('Snippet header component', () => {
title: x.attributes('title'),
text: x.text(),
}));
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
gon.relative_url_root = '/foo/';
@@ -113,28 +122,12 @@ describe('Snippet header component', () => {
createdAt: new Date(differenceInMilliseconds(32 * 24 * 3600 * 1000)).toISOString(),
};
- mutationVariables = {
- mutation: DeleteSnippetMutation,
- variables: {
- id: snippet.id,
- },
- };
-
- errorMsg = 'Foo bar';
- err = { message: errorMsg };
-
- mutationTypes = {
- RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
- REJECT: jest.fn(() => Promise.reject(err)),
- };
-
mock = new MockAdapter(axios);
});
afterEach(() => {
- wrapper.destroy();
+ mockApollo = null;
mock.restore();
- gon.relative_url_root = originalRelativeUrlRoot;
});
it('renders itself', () => {
@@ -238,15 +231,16 @@ describe('Snippet header component', () => {
});
it('with canCreateSnippet permission, renders create button', async () => {
- createComponent();
+ createComponent({
+ canCreateProjectSnippetMock: jest
+ .fn()
+ .mockResolvedValue(getCanCreateProjectSnippetMock(true)),
+ canCreatePersonalSnippetMock: jest
+ .fn()
+ .mockResolvedValue(getCanCreatePersonalSnippetMock(true)),
+ });
- // TODO: we should avoid `wrapper.setData` since they
- // are component internals. Let's use the apollo mock helpers
- // in a follow-up.
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ canCreateSnippet: true });
- await nextTick();
+ await waitForPromises();
expect(findButtonsAsModel()).toEqual(
expect.arrayContaining([
@@ -271,7 +265,7 @@ describe('Snippet header component', () => {
${200} | ${VARIANT_SUCCESS} | ${i18n.snippetSpamSuccess}
${500} | ${VARIANT_DANGER} | ${i18n.snippetSpamFailure}
`(
- 'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
+ 'renders a "$variant" alert message with "$text" message for a request with a "$request" response',
async ({ request, variant, text }) => {
const submitAsSpamBtn = findButtons().at(2);
mock.onPost(reportAbusePath).reply(request);
@@ -329,21 +323,37 @@ describe('Snippet header component', () => {
});
describe('Delete mutation', () => {
- it('dispatches a mutation to delete the snippet with correct variables', () => {
+ const deleteSnippet = async () => {
+ // Click delete action
+ findButtons().at(1).trigger('click');
+ await nextTick();
+
+ expect(findDeleteModal().props().visible).toBe(true);
+
+ // Click delete button in delete modal
+ document.querySelector('[data-testid="delete-snippet"').click();
+ await waitForPromises();
+ };
+
+ it('dispatches a mutation to delete the snippet with correct variables', async () => {
createComponent();
- wrapper.vm.deleteSnippet();
- expect(mutationTypes.RESOLVE).toHaveBeenCalledWith(mutationVariables);
+
+ await deleteSnippet();
+
+ expect(MUTATION_TYPES.RESOLVE).toHaveBeenCalledWith({
+ id: snippet.id,
+ });
});
it('sets error message if mutation fails', async () => {
- createComponent({ mutationRes: mutationTypes.REJECT });
+ createComponent({ deleteSnippetMock: MUTATION_TYPES.REJECT });
expect(Boolean(wrapper.vm.errorMessage)).toBe(false);
- wrapper.vm.deleteSnippet();
-
- await waitForPromises();
+ await deleteSnippet();
- expect(wrapper.vm.errorMessage).toEqual(errorMsg);
+ expect(document.querySelector('[data-testid="delete-alert"').textContent.trim()).toBe(
+ ERROR_MSG,
+ );
});
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
@@ -353,15 +363,16 @@ describe('Snippet header component', () => {
createComponent({
snippetProps,
});
- wrapper.vm.closeDeleteModal = jest.fn();
- wrapper.vm.deleteSnippet();
- await nextTick();
+ await deleteSnippet();
};
it('redirects to dashboard/snippets for personal snippet', async () => {
await createDeleteSnippet();
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+
+ // Check that the modal is hidden after deleting the snippet
+ expect(findDeleteModal().props().visible).toBe(false);
+
expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`);
});
@@ -372,7 +383,10 @@ describe('Snippet header component', () => {
fullPath,
},
});
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+
+ // Check that the modal is hidden after deleting the snippet
+ expect(findDeleteModal().props().visible).toBe(false);
+
expect(window.location.pathname).toBe(`${fullPath}/-/snippets`);
});
});
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index 7c40735d64e..0a3b57c9244 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -26,10 +26,6 @@ describe('Snippet header component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders itself', () => {
createComponent();
expect(wrapper.find('.snippet-header').exists()).toBe(true);
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 29eb002ef4a..70eb719f706 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -51,10 +51,6 @@ describe('Snippet Visibility Edit component', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
diff --git a/spec/frontend/snippets/mock_data.js b/spec/frontend/snippets/mock_data.js
new file mode 100644
index 00000000000..7546fa575c6
--- /dev/null
+++ b/spec/frontend/snippets/mock_data.js
@@ -0,0 +1,19 @@
+export const getCanCreateProjectSnippetMock = (createSnippet = false) => ({
+ data: {
+ project: {
+ userPermissions: {
+ createSnippet,
+ },
+ },
+ },
+});
+
+export const getCanCreatePersonalSnippetMock = (createSnippet = false) => ({
+ data: {
+ currentUser: {
+ userPermissions: {
+ createSnippet,
+ },
+ },
+ },
+});
diff --git a/spec/frontend/streaming/chunk_writer_spec.js b/spec/frontend/streaming/chunk_writer_spec.js
new file mode 100644
index 00000000000..2aadb332838
--- /dev/null
+++ b/spec/frontend/streaming/chunk_writer_spec.js
@@ -0,0 +1,214 @@
+import { ChunkWriter } from '~/streaming/chunk_writer';
+import { RenderBalancer } from '~/streaming/render_balancer';
+
+jest.mock('~/streaming/render_balancer');
+
+describe('ChunkWriter', () => {
+ let accumulator = '';
+ let write;
+ let close;
+ let abort;
+ let config;
+ let render;
+
+ const createChunk = (text) => {
+ const encoder = new TextEncoder();
+ return encoder.encode(text);
+ };
+
+ const createHtmlStream = () => {
+ write = jest.fn((part) => {
+ accumulator += part;
+ });
+ close = jest.fn();
+ abort = jest.fn();
+ return {
+ write,
+ close,
+ abort,
+ };
+ };
+
+ const createWriter = () => {
+ return new ChunkWriter(createHtmlStream(), config);
+ };
+
+ const pushChunks = (...chunks) => {
+ const writer = createWriter();
+ chunks.forEach((chunk) => {
+ writer.write(createChunk(chunk));
+ });
+ writer.close();
+ };
+
+ afterAll(() => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
+ });
+
+ beforeEach(() => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = 100;
+ accumulator = '';
+ config = undefined;
+ render = jest.fn((cb) => {
+ while (cb()) {
+ // render until 'false'
+ }
+ });
+ RenderBalancer.mockImplementation(() => ({ render }));
+ });
+
+ describe('when chunk length must be "1"', () => {
+ beforeEach(() => {
+ config = { minChunkSize: 1, maxChunkSize: 1 };
+ });
+
+ it('splits big chunks into smaller ones', () => {
+ const text = 'foobar';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(text.length);
+ });
+
+ it('handles small emoji chunks', () => {
+ const text = 'foo👀bar👨‍👩‍👧baz👧👧🏻👧🏼👧🏽👧🏾👧🏿';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(createChunk(text).length);
+ });
+ });
+
+ describe('when chunk length must not be lower than "5" and exceed "10"', () => {
+ beforeEach(() => {
+ config = { minChunkSize: 5, maxChunkSize: 10 };
+ });
+
+ it('joins small chunks', () => {
+ const text = '12345';
+ pushChunks(...text.split(''));
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(1);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles overflow with small chunks', () => {
+ const text = '123456789';
+ pushChunks(...text.split(''));
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(2);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls flush on small chunks', () => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
+ const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator');
+ const text = '1';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(flushAccumulator).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls flush on large chunks', () => {
+ const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator');
+ const text = '1234567890123';
+ const writer = createWriter();
+ writer.write(createChunk(text));
+ jest.runAllTimers();
+ expect(accumulator).toBe(text);
+ expect(flushAccumulator).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('chunk balancing', () => {
+ let increase;
+ let decrease;
+ let renderOnce;
+
+ beforeEach(() => {
+ render = jest.fn((cb) => {
+ let next = true;
+ renderOnce = () => {
+ if (!next) return;
+ next = cb();
+ };
+ });
+ RenderBalancer.mockImplementation(({ increase: inc, decrease: dec }) => {
+ increase = jest.fn(inc);
+ decrease = jest.fn(dec);
+ return {
+ render,
+ };
+ });
+ });
+
+ describe('when frame time exceeds low limit', () => {
+ beforeEach(() => {
+ config = {
+ minChunkSize: 1,
+ maxChunkSize: 5,
+ balanceRate: 10,
+ };
+ });
+
+ it('increases chunk size', () => {
+ const text = '111222223';
+ const writer = createWriter();
+ const chunk = createChunk(text);
+
+ writer.write(chunk);
+
+ renderOnce();
+ increase();
+ renderOnce();
+ renderOnce();
+
+ writer.close();
+
+ expect(accumulator).toBe(text);
+ expect(write.mock.calls).toMatchObject([['111'], ['22222'], ['3']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when frame time exceeds high limit', () => {
+ beforeEach(() => {
+ config = {
+ minChunkSize: 1,
+ maxChunkSize: 10,
+ balanceRate: 2,
+ };
+ });
+
+ it('decreases chunk size', () => {
+ const text = '1111112223345';
+ const writer = createWriter();
+ const chunk = createChunk(text);
+
+ writer.write(chunk);
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ renderOnce();
+
+ writer.close();
+
+ expect(accumulator).toBe(text);
+ expect(write.mock.calls).toMatchObject([['111111'], ['222'], ['33'], ['4'], ['5']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ it('calls abort on htmlStream', () => {
+ const writer = createWriter();
+ writer.abort();
+ expect(abort).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/streaming/handle_streamed_anchor_link_spec.js b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js
new file mode 100644
index 00000000000..ef17957b2fc
--- /dev/null
+++ b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js
@@ -0,0 +1,132 @@
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import LineHighlighter from '~/blob/line_highlighter';
+import { TEST_HOST } from 'spec/test_constants';
+
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/blob/line_highlighter');
+
+describe('handleStreamedAnchorLink', () => {
+ const ANCHOR_START = 'L100';
+ const ANCHOR_END = '300';
+ const findRoot = () => document.querySelector('#root');
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('when single line anchor is given', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}#${ANCHOR_START}`);
+ });
+
+ describe('when element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"><div id="${ANCHOR_START}"></div></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when element is streamed', () => {
+ let stop;
+ const insertElement = () => {
+ findRoot().insertAdjacentHTML('afterbegin', `<div id="${ANCHOR_START}"></div>`);
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ stop = handleStreamedAnchorLink(findRoot());
+ });
+
+ afterEach(() => {
+ stop = undefined;
+ });
+
+ it('scrolls to the anchor when inserted', async () => {
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ expect(LineHighlighter).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't scroll to the anchor when destroyed", async () => {
+ stop();
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when line range anchor is given', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}#${ANCHOR_START}-${ANCHOR_END}`);
+ });
+
+ describe('when last element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"><div id="L${ANCHOR_END}"></div></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when last element is streamed', () => {
+ let stop;
+ const insertElement = () => {
+ findRoot().insertAdjacentHTML(
+ 'afterbegin',
+ `<div id="${ANCHOR_START}"></div><div id="L${ANCHOR_END}"></div>`,
+ );
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ stop = handleStreamedAnchorLink(findRoot());
+ });
+
+ afterEach(() => {
+ stop = undefined;
+ });
+
+ it('scrolls to the anchor when inserted', async () => {
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ expect(LineHighlighter).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't scroll to the anchor when destroyed", async () => {
+ stop();
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when anchor is not given', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/streaming/html_stream_spec.js b/spec/frontend/streaming/html_stream_spec.js
new file mode 100644
index 00000000000..115a9ddc803
--- /dev/null
+++ b/spec/frontend/streaming/html_stream_spec.js
@@ -0,0 +1,46 @@
+import { HtmlStream } from '~/streaming/html_stream';
+import { ChunkWriter } from '~/streaming/chunk_writer';
+
+jest.mock('~/streaming/chunk_writer');
+
+describe('HtmlStream', () => {
+ let write;
+ let close;
+ let streamingElement;
+
+ beforeEach(() => {
+ write = jest.fn();
+ close = jest.fn();
+ jest.spyOn(Document.prototype, 'write').mockImplementation(write);
+ jest.spyOn(Document.prototype, 'close').mockImplementation(close);
+ jest.spyOn(Document.prototype, 'querySelector').mockImplementation(() => {
+ streamingElement = document.createElement('div');
+ return streamingElement;
+ });
+ });
+
+ it('attaches to original document', () => {
+ // eslint-disable-next-line no-new
+ new HtmlStream(document.body);
+ expect(document.body.contains(streamingElement)).toBe(true);
+ });
+
+ it('can write to a document', () => {
+ const htmlStream = new HtmlStream(document.body);
+ htmlStream.write('foo');
+ htmlStream.close();
+ expect(write.mock.calls).toEqual([['<streaming-element>'], ['foo'], ['</streaming-element>']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns chunked writer', () => {
+ const htmlStream = new HtmlStream(document.body).withChunkWriter();
+ expect(htmlStream).toBeInstanceOf(ChunkWriter);
+ });
+
+ it('closes on abort', () => {
+ const htmlStream = new HtmlStream(document.body);
+ htmlStream.abort();
+ expect(close).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/streaming/rate_limit_stream_requests_spec.js b/spec/frontend/streaming/rate_limit_stream_requests_spec.js
new file mode 100644
index 00000000000..02e3cf93014
--- /dev/null
+++ b/spec/frontend/streaming/rate_limit_stream_requests_spec.js
@@ -0,0 +1,155 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
+
+describe('rateLimitStreamRequests', () => {
+ const encoder = new TextEncoder('utf-8');
+ const createStreamResponse = (content = 'foo') =>
+ new ReadableStream({
+ pull(controller) {
+ controller.enqueue(encoder.encode(content));
+ controller.close();
+ },
+ });
+
+ const createFactory = (content) => {
+ return jest.fn(() => {
+ return Promise.resolve(createStreamResponse(content));
+ });
+ };
+
+ it('does nothing for zero total requests', () => {
+ const factory = jest.fn();
+ const requests = rateLimitStreamRequests({
+ factory,
+ total: 0,
+ });
+ expect(factory).toHaveBeenCalledTimes(0);
+ expect(requests.length).toBe(0);
+ });
+
+ it('does not exceed total requests', () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ immediateCount: 100,
+ maxConcurrentRequests: 100,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(2);
+ });
+
+ it('creates immediate requests', () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(2);
+ });
+
+ it('returns correct values', async () => {
+ const fixture = 'foobar';
+ const factory = createFactory(fixture);
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 2,
+ });
+
+ const decoder = new TextDecoder('utf-8');
+ let result = '';
+ for await (const stream of requests) {
+ await stream.pipeTo(
+ new WritableStream({
+ // eslint-disable-next-line no-loop-func
+ write(content) {
+ result += decoder.decode(content);
+ },
+ }),
+ );
+ }
+
+ expect(result).toBe(fixture + fixture);
+ });
+
+ it('delays rate limited requests', async () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 3,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(3);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(3);
+ });
+
+ it('runs next request after previous has been fulfilled', async () => {
+ let res;
+ const factory = jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ res = resolve;
+ }),
+ )
+ .mockImplementationOnce(() => Promise.resolve(createStreamResponse()));
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 1,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(requests.length).toBe(2);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(1);
+
+ res(createStreamResponse());
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(2);
+ });
+
+ it('uses timer to schedule next request', async () => {
+ let res;
+ const factory = jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ res = resolve;
+ }),
+ )
+ .mockImplementationOnce(() => Promise.resolve(createStreamResponse()));
+ const requests = rateLimitStreamRequests({
+ factory,
+ immediateCount: 1,
+ maxConcurrentRequests: 2,
+ total: 2,
+ timeout: 9999,
+ });
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(requests.length).toBe(2);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(1);
+
+ jest.runAllTimers();
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(2);
+ res(createStreamResponse());
+ });
+});
diff --git a/spec/frontend/streaming/render_balancer_spec.js b/spec/frontend/streaming/render_balancer_spec.js
new file mode 100644
index 00000000000..dae0c98d678
--- /dev/null
+++ b/spec/frontend/streaming/render_balancer_spec.js
@@ -0,0 +1,69 @@
+import { RenderBalancer } from '~/streaming/render_balancer';
+
+const HIGH_FRAME_TIME = 100;
+const LOW_FRAME_TIME = 10;
+
+describe('renderBalancer', () => {
+ let frameTime = 0;
+ let frameTimeDelta = 0;
+ let decrease;
+ let increase;
+
+ const createBalancer = () => {
+ decrease = jest.fn();
+ increase = jest.fn();
+ return new RenderBalancer({
+ highFrameTime: HIGH_FRAME_TIME,
+ lowFrameTime: LOW_FRAME_TIME,
+ increase,
+ decrease,
+ });
+ };
+
+ const renderTimes = (times) => {
+ const balancer = createBalancer();
+ return new Promise((resolve) => {
+ let counter = 0;
+ balancer.render(() => {
+ if (counter === times) {
+ resolve(counter);
+ return false;
+ }
+ counter += 1;
+ return true;
+ });
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
+ frameTime += frameTimeDelta;
+ cb(frameTime);
+ });
+ });
+
+ afterEach(() => {
+ window.requestAnimationFrame.mockRestore();
+ frameTime = 0;
+ frameTimeDelta = 0;
+ });
+
+ it('renders in a loop', async () => {
+ const count = await renderTimes(5);
+ expect(count).toBe(5);
+ });
+
+ it('calls decrease', async () => {
+ frameTimeDelta = 200;
+ await renderTimes(5);
+ expect(decrease).toHaveBeenCalled();
+ expect(increase).not.toHaveBeenCalled();
+ });
+
+ it('calls increase', async () => {
+ frameTimeDelta = 1;
+ await renderTimes(5);
+ expect(increase).toHaveBeenCalled();
+ expect(decrease).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/streaming/render_html_streams_spec.js b/spec/frontend/streaming/render_html_streams_spec.js
new file mode 100644
index 00000000000..55cef0ea469
--- /dev/null
+++ b/spec/frontend/streaming/render_html_streams_spec.js
@@ -0,0 +1,96 @@
+import { ReadableStream } from 'node:stream/web';
+import { renderHtmlStreams } from '~/streaming/render_html_streams';
+import { HtmlStream } from '~/streaming/html_stream';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/streaming/html_stream');
+jest.mock('~/streaming/constants', () => {
+ return {
+ HIGH_FRAME_TIME: 0,
+ LOW_FRAME_TIME: 0,
+ MAX_CHUNK_SIZE: 1,
+ MIN_CHUNK_SIZE: 1,
+ };
+});
+
+const firstStreamContent = 'foobar';
+const secondStreamContent = 'bazqux';
+
+describe('renderHtmlStreams', () => {
+ let htmlWriter;
+ const encoder = new TextEncoder();
+ const createSingleChunkStream = (chunk) => {
+ const encoded = encoder.encode(chunk);
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(encoded);
+ controller.close();
+ },
+ });
+ return [stream, encoded];
+ };
+
+ beforeEach(() => {
+ htmlWriter = {
+ write: jest.fn(),
+ close: jest.fn(),
+ abort: jest.fn(),
+ };
+ jest.spyOn(HtmlStream.prototype, 'withChunkWriter').mockReturnValue(htmlWriter);
+ });
+
+ it('renders a single stream', async () => {
+ const [stream, encoded] = createSingleChunkStream(firstStreamContent);
+
+ await renderHtmlStreams([Promise.resolve(stream)], document.body);
+
+ expect(htmlWriter.write).toHaveBeenCalledWith(encoded);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders stream sequence', async () => {
+ const [stream1, encoded1] = createSingleChunkStream(firstStreamContent);
+ const [stream2, encoded2] = createSingleChunkStream(secondStreamContent);
+
+ await renderHtmlStreams([Promise.resolve(stream1), Promise.resolve(stream2)], document.body);
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't wait for the whole sequence to resolve before streaming", async () => {
+ const [stream1, encoded1] = createSingleChunkStream(firstStreamContent);
+ const [stream2, encoded2] = createSingleChunkStream(secondStreamContent);
+
+ let res;
+ const delayedStream = new Promise((resolve) => {
+ res = resolve;
+ });
+
+ renderHtmlStreams([Promise.resolve(stream1), delayedStream], document.body);
+
+ await waitForPromises();
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(0);
+
+ res(stream2);
+ await waitForPromises();
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('closes HtmlStream on error', async () => {
+ const [stream1] = createSingleChunkStream(firstStreamContent);
+ const error = new Error();
+
+ try {
+ await renderHtmlStreams([Promise.resolve(stream1), Promise.reject(error)], document.body);
+ } catch (err) {
+ expect(err).toBe(error);
+ }
+
+ expect(htmlWriter.abort).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
new file mode 100644
index 00000000000..538e87cf843
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js
@@ -0,0 +1,219 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSearchBoxByType } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { s__ } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
+import ProjectsList from '~/super_sidebar/components/projects_list.vue';
+import GroupsList from '~/super_sidebar/components/groups_list.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import searchUserProjectsAndGroupsQuery from '~/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql';
+import { trackContextAccess, formatContextSwitcherItems } from '~/super_sidebar/utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
+import { searchUserProjectsAndGroupsResponseMock } from '../mock_data';
+
+jest.mock('~/super_sidebar/utils', () => ({
+ getStorageKeyFor: jest.requireActual('~/super_sidebar/utils').getStorageKeyFor,
+ getTopFrequentItems: jest.requireActual('~/super_sidebar/utils').getTopFrequentItems,
+ formatContextSwitcherItems: jest.requireActual('~/super_sidebar/utils')
+ .formatContextSwitcherItems,
+ trackContextAccess: jest.fn(),
+}));
+
+const username = 'root';
+const projectsPath = 'projectsPath';
+const groupsPath = 'groupsPath';
+
+Vue.use(VueApollo);
+
+describe('ContextSwitcher component', () => {
+ let wrapper;
+ let mockApollo;
+
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findProjectsList = () => wrapper.findComponent(ProjectsList);
+ const findGroupsList = () => wrapper.findComponent(GroupsList);
+
+ const triggerSearchQuery = async () => {
+ findSearchBox().vm.$emit('input', 'foo');
+ await nextTick();
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ return waitForPromises();
+ };
+
+ const searchUserProjectsAndGroupsHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(searchUserProjectsAndGroupsResponseMock);
+
+ const createWrapper = ({ props = {}, requestHandlers = {} } = {}) => {
+ mockApollo = createMockApollo([
+ [
+ searchUserProjectsAndGroupsQuery,
+ requestHandlers.searchUserProjectsAndGroupsQueryHandler ??
+ searchUserProjectsAndGroupsHandlerSuccess,
+ ],
+ ]);
+
+ wrapper = shallowMountExtended(ContextSwitcher, {
+ apolloProvider: mockApollo,
+ propsData: {
+ username,
+ projectsPath,
+ groupsPath,
+ ...props,
+ },
+ stubs: {
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ props: ['placeholder'],
+ }),
+ ProjectsList: stubComponent(ProjectsList, {
+ props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
+ }),
+ GroupsList: stubComponent(GroupsList, {
+ props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
+ }),
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes the placeholder to the search box', () => {
+ expect(findSearchBox().props('placeholder')).toBe(
+ s__('Navigation|Search for projects or groups'),
+ );
+ });
+
+ it('passes the correct props the frequent projects list', () => {
+ expect(findProjectsList().props()).toEqual({
+ username,
+ viewAllLink: projectsPath,
+ isSearch: false,
+ searchResults: [],
+ });
+ });
+
+ it('passes the correct props the frequent groups list', () => {
+ expect(findGroupsList().props()).toEqual({
+ username,
+ viewAllLink: groupsPath,
+ isSearch: false,
+ searchResults: [],
+ });
+ });
+
+ it('does not trigger the search query on mount', () => {
+ expect(searchUserProjectsAndGroupsHandlerSuccess).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('item access tracking', () => {
+ it('does not track anything if not within a trackable context', () => {
+ createWrapper();
+
+ expect(trackContextAccess).not.toHaveBeenCalled();
+ });
+
+ it('tracks item access if within a trackable context', () => {
+ const currentContext = { namespace: 'groups' };
+ createWrapper({
+ props: {
+ currentContext,
+ },
+ });
+
+ expect(trackContextAccess).toHaveBeenCalledWith(username, currentContext);
+ });
+ });
+
+ describe('on search', () => {
+ beforeEach(() => {
+ createWrapper();
+ return triggerSearchQuery();
+ });
+
+ it('triggers the search query on search', () => {
+ expect(searchUserProjectsAndGroupsHandlerSuccess).toHaveBeenCalled();
+ });
+
+ it('passes the projects to the frequent projects list', () => {
+ expect(findProjectsList().props('isSearch')).toBe(true);
+ expect(findProjectsList().props('searchResults')).toEqual(
+ formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.projects.nodes),
+ );
+ });
+
+ it('passes the groups to the frequent groups list', () => {
+ expect(findGroupsList().props('isSearch')).toBe(true);
+ expect(findGroupsList().props('searchResults')).toEqual(
+ formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.user.groups.nodes),
+ );
+ });
+ });
+
+ describe('when search query does not match any items', () => {
+ beforeEach(() => {
+ createWrapper({
+ requestHandlers: {
+ searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
+ data: {
+ projects: {
+ nodes: [],
+ },
+ user: {
+ id: '1',
+ groups: {
+ nodes: [],
+ },
+ },
+ },
+ }),
+ },
+ });
+ return triggerSearchQuery();
+ });
+
+ it('passes empty results to the lists', () => {
+ expect(findProjectsList().props('isSearch')).toBe(true);
+ expect(findProjectsList().props('searchResults')).toEqual([]);
+ expect(findGroupsList().props('isSearch')).toBe(true);
+ expect(findGroupsList().props('searchResults')).toEqual([]);
+ });
+ });
+
+ describe('when search query fails', () => {
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException');
+ });
+
+ it('captures exception if response is formatted incorrectly', async () => {
+ createWrapper({
+ requestHandlers: {
+ searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
+ data: {},
+ }),
+ },
+ });
+ await triggerSearchQuery();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+
+ it('captures exception if query fails', async () => {
+ createWrapper({
+ requestHandlers: {
+ searchUserProjectsAndGroupsQueryHandler: jest.fn().mockRejectedValue(),
+ },
+ });
+ await triggerSearchQuery();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
new file mode 100644
index 00000000000..7172b60d0fa
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
@@ -0,0 +1,50 @@
+import { GlAvatar } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue';
+
+describe('ContextSwitcherToggle component', () => {
+ let wrapper;
+
+ const context = {
+ id: 1,
+ title: 'Title',
+ avatar: '/path/to/avatar.png',
+ };
+
+ const findGlAvatar = () => wrapper.getComponent(GlAvatar);
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(ContextSwitcherToggle, {
+ propsData: {
+ context,
+ expanded: false,
+ ...props,
+ },
+ });
+ };
+
+ describe('with an avatar', () => {
+ it('passes the correct props to GlAvatar', () => {
+ createWrapper();
+ const avatar = findGlAvatar();
+
+ expect(avatar.props('shape')).toBe('rect');
+ expect(avatar.props('entityName')).toBe(context.title);
+ expect(avatar.props('entityId')).toBe(context.id);
+ expect(avatar.props('src')).toBe(context.avatar);
+ });
+
+ it('renders the avatar with a custom shape', () => {
+ const customShape = 'circle';
+ createWrapper({
+ context: {
+ ...context,
+ avatar_shape: customShape,
+ },
+ });
+ const avatar = findGlAvatar();
+
+ expect(avatar.props('shape')).toBe(customShape);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
new file mode 100644
index 00000000000..1e98db091f2
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
@@ -0,0 +1,68 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import FrequentItemsList from '~/super_sidebar/components//frequent_items_list.vue';
+import ItemsList from '~/super_sidebar/components/items_list.vue';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { cachedFrequentProjects } from '../mock_data';
+
+const title = s__('Navigation|FREQUENT PROJECTS');
+const pristineText = s__('Navigation|Projects you visit often will appear here.');
+const storageKey = 'storageKey';
+const maxItems = 5;
+
+describe('FrequentItemsList component', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ const findListTitle = () => wrapper.findByTestId('list-title');
+ const findItemsList = () => wrapper.findComponent(ItemsList);
+ const findEmptyText = () => wrapper.findByTestId('empty-text');
+
+ const createWrapper = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(FrequentItemsList, {
+ propsData: {
+ title,
+ pristineText,
+ storageKey,
+ maxItems,
+ ...props,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it("renders the list's title", () => {
+ expect(findListTitle().text()).toBe(title);
+ });
+
+ it('renders the empty text', () => {
+ expect(findEmptyText().exists()).toBe(true);
+ expect(findEmptyText().text()).toBe(pristineText);
+ });
+ });
+
+ describe('when there are cached frequent items', () => {
+ beforeEach(() => {
+ window.localStorage.setItem(storageKey, cachedFrequentProjects);
+ createWrapper();
+ });
+
+ it('attempts to retrieve the items from the local storage', () => {
+ expect(window.localStorage.getItem).toHaveBeenCalledTimes(1);
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(storageKey);
+ });
+
+ it('renders the maximum amount of items', () => {
+ expect(findItemsList().props('items').length).toBe(maxItems);
+ });
+
+ it('does not render the empty text slot', () => {
+ expect(findEmptyText().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
new file mode 100644
index 00000000000..e5ba1c63996
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
@@ -0,0 +1,238 @@
+import { GlDropdownItem, GlLoadingIcon, GlAvatar, GlAlert, GlDropdownDivider } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+import {
+ LARGE_AVATAR_PX,
+ SMALL_AVATAR_PX,
+} from '~/super_sidebar/components/global_search/constants';
+import {
+ PROJECTS_CATEGORY,
+ GROUPS_CATEGORY,
+ ISSUES_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ RECENT_EPICS_CATEGORY,
+} from '~/vue_shared/global_search/constants';
+import {
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP,
+ MOCK_SEARCH,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2,
+} from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('HeaderSearchAutocompleteItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, mockGetters, props) => {
+ const store = new Vuex.Store({
+ state: {
+ loading: false,
+ ...initialState,
+ },
+ getters: {
+ autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMount(HeaderSearchAutocompleteItems, {
+ store,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
+ const findFirstDropdownItem = () => findDropdownItems().at(0);
+ const findDropdownItemTitles = () =>
+ findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text());
+ const findDropdownItemSubTitles = () =>
+ findDropdownItems()
+ .wrappers.filter((w) => w.findAll('span').length > 2)
+ .map((w) => w.findAll('span').at(2).text());
+ const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGlAvatar = () => wrapper.findComponent(GlAvatar);
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('template', () => {
+ describe('when loading is true', () => {
+ beforeEach(() => {
+ createComponent({ loading: true });
+ });
+
+ it('renders GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render autocomplete options', () => {
+ expect(findDropdownItems()).toHaveLength(0);
+ });
+ });
+
+ describe('when api returns error', () => {
+ beforeEach(() => {
+ createComponent({ autocompleteError: true });
+ });
+
+ it('renders Alert', () => {
+ expect(findGlAlert().exists()).toBe(true);
+ });
+ });
+ describe('when loading is false', () => {
+ beforeEach(() => {
+ createComponent({ loading: false });
+ });
+
+ it('does not render GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('Dropdown items', () => {
+ it('renders item for each option in autocomplete option', () => {
+ expect(findDropdownItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label);
+ expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders sub-titles correctly', () => {
+ const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
+ (o) => o.label,
+ );
+ expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
+ expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+
+ describe.each`
+ item | showAvatar | avatarSize | searchContext | entityId | entityName
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''}
+ ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''}
+ ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'}
+ `('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => {
+ describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
+ beforeEach(() => {
+ createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] });
+ });
+
+ it(`should${showAvatar ? '' : ' not'} render`, () => {
+ expect(findGlAvatar().exists()).toBe(showAvatar);
+ });
+
+ it(`should set avatarSize to ${avatarSize}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
+ });
+
+ it(`should set avatar entityId to ${entityId}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe(
+ entityName,
+ );
+ });
+ });
+ });
+ });
+
+ describe.each`
+ currentFocusedOption | isFocused | ariaSelected
+ ${null} | ${false} | ${undefined}
+ ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
+ ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0]} | ${true} | ${'true'}
+ `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
+ describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
+ beforeEach(() => {
+ createComponent({}, {}, { currentFocusedOption });
+ });
+
+ it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
+ expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
+ });
+
+ it(`sets "aria-selected to ${ariaSelected}`, () => {
+ expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ });
+ });
+ });
+
+ describe.each`
+ search | items | dividerCount
+ ${null} | ${[]} | ${0}
+ ${''} | ${[]} | ${0}
+ ${'1'} | ${[]} | ${0}
+ ${')'} | ${[]} | ${0}
+ ${'t'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP} | ${1}
+ ${'te'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP} | ${0}
+ ${'tes'} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
+ ${MOCK_SEARCH} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2} | ${1}
+ `('Header Search Dropdown Dividers', ({ search, items, dividerCount }) => {
+ describe(`when search is ${search}`, () => {
+ beforeEach(() => {
+ createComponent(
+ { search },
+ {
+ autocompleteGroupedSearchOptions: () => items,
+ },
+ {},
+ );
+ });
+
+ it(`component should have ${dividerCount} dividers`, () => {
+ expect(findGlDropdownDividers()).toHaveLength(dividerCount);
+ });
+ });
+ });
+ });
+
+ describe('watchers', () => {
+ describe('currentFocusedOption', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('when focused changes to existing element calls scroll into view on the newly focused element', async () => {
+ const focusedElement = findFirstDropdownItem().element;
+ const scrollSpy = jest.spyOn(focusedElement, 'scrollIntoView');
+
+ wrapper.setProps({ currentFocusedOption: MOCK_SORTED_AUTOCOMPLETE_OPTIONS[0] });
+
+ await nextTick();
+
+ expect(scrollSpy).toHaveBeenCalledWith(false);
+ scrollSpy.mockRestore();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
new file mode 100644
index 00000000000..132f8e60598
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
@@ -0,0 +1,102 @@
+import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('HeaderSearchDefaultItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, props) => {
+ const store = new Vuex.Store({
+ state: {
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ },
+ getters: {
+ defaultSearchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ },
+ });
+
+ wrapper = shallowMount(HeaderSearchDefaultItems, {
+ store,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findFirstDropdownItem = () => findDropdownItems().at(0);
+ const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
+ const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+
+ describe('template', () => {
+ describe('Dropdown items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item for each option in defaultSearchOptions', () => {
+ expect(findDropdownItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ const expectedTitles = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.title);
+ expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_DEFAULT_SEARCH_OPTIONS.map((o) => o.url);
+ expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+
+ describe.each`
+ group | project | dropdownTitle
+ ${null} | ${null} | ${'All GitLab'}
+ ${{ name: 'Test Group' }} | ${null} | ${'Test Group'}
+ ${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'}
+ `('Dropdown Header', ({ group, project, dropdownTitle }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createComponent({
+ searchContext: {
+ group,
+ project,
+ },
+ });
+ });
+
+ it(`should render as ${dropdownTitle}`, () => {
+ expect(findDropdownHeader().text()).toBe(dropdownTitle);
+ });
+ });
+ });
+
+ describe.each`
+ currentFocusedOption | isFocused | ariaSelected
+ ${null} | ${false} | ${undefined}
+ ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
+ ${MOCK_DEFAULT_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
+ `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
+ describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
+ beforeEach(() => {
+ createComponent({}, { currentFocusedOption });
+ });
+
+ it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
+ expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
+ });
+
+ it(`sets "aria-selected to ${ariaSelected}`, () => {
+ expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
new file mode 100644
index 00000000000..fa91ef43ced
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
@@ -0,0 +1,120 @@
+import { GlDropdownItem, GlToken, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
+import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import { truncate } from '~/lib/utils/text_utility';
+import { SCOPE_TOKEN_MAX_LENGTH } from '~/super_sidebar/components/global_search/constants';
+import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants';
+import {
+ MOCK_SEARCH,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('HeaderSearchScopedItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, mockGetters, props) => {
+ const store = new Vuex.Store({
+ state: {
+ search: MOCK_SEARCH,
+ ...initialState,
+ },
+ getters: {
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMount(HeaderSearchScopedItems, {
+ store,
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findFirstDropdownItem = () => findDropdownItems().at(0);
+ const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
+ const findScopeTokens = () => wrapper.findAllComponents(GlToken);
+ const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text()));
+ const findScopeTokensIcons = () =>
+ findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon));
+ const findDropdownItemAriaLabels = () =>
+ findDropdownItems().wrappers.map((w) => trimText(w.attributes('aria-label')));
+ const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
+
+ describe('template', () => {
+ describe('Dropdown items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item for each option in scopedSearchOptions', () => {
+ expect(findDropdownItems()).toHaveLength(MOCK_SCOPED_SEARCH_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ findDropdownItemTitles().forEach((title) => expect(title).toContain(MOCK_SEARCH));
+ });
+
+ it('renders scope names correctly', () => {
+ const expectedTitles = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
+ truncate(trimText(`in ${o.description || o.scope}`), SCOPE_TOKEN_MAX_LENGTH),
+ );
+
+ expect(findScopeTokensText()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders scope icons correctly', () => {
+ findScopeTokensIcons().forEach((icon, i) => {
+ const w = icon.wrappers[0];
+ expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_OPTIONS[i].icon);
+ });
+ });
+
+ it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => {
+ expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false);
+ });
+
+ it('renders aria-labels correctly', () => {
+ const expectedLabels = MOCK_SCOPED_SEARCH_OPTIONS.map((o) =>
+ trimText(`${MOCK_SEARCH} ${o.description || o.icon} ${o.scope || ''}`),
+ );
+ expect(findDropdownItemAriaLabels()).toStrictEqual(expectedLabels);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SCOPED_SEARCH_OPTIONS.map((o) => o.url);
+ expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+
+ describe.each`
+ currentFocusedOption | isFocused | ariaSelected
+ ${null} | ${false} | ${undefined}
+ ${{ html_id: 'not-a-match' }} | ${false} | ${undefined}
+ ${MOCK_SCOPED_SEARCH_OPTIONS[0]} | ${true} | ${'true'}
+ `('isOptionFocused', ({ currentFocusedOption, isFocused, ariaSelected }) => {
+ describe(`when currentFocusedOption.html_id is ${currentFocusedOption?.html_id}`, () => {
+ beforeEach(() => {
+ createComponent({}, {}, { currentFocusedOption });
+ });
+
+ it(`should${isFocused ? '' : ' not'} have gl-bg-gray-50 applied`, () => {
+ expect(findFirstDropdownItem().classes('gl-bg-gray-50')).toBe(isFocused);
+ });
+
+ it(`sets "aria-selected to ${ariaSelected}`, () => {
+ expect(findFirstDropdownItem().attributes('aria-selected')).toBe(ariaSelected);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
new file mode 100644
index 00000000000..0dcfc448125
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -0,0 +1,516 @@
+import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import { s__, sprintf } from '~/locale';
+import HeaderSearchApp from '~/super_sidebar/components/global_search/components/global_search.vue';
+import HeaderSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+import HeaderSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import HeaderSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import {
+ SEARCH_INPUT_DESCRIPTION,
+ SEARCH_RESULTS_DESCRIPTION,
+ SEARCH_BOX_INDEX,
+ ICON_PROJECT,
+ ICON_GROUP,
+ ICON_SUBGROUP,
+ SCOPE_TOKEN_MAX_LENGTH,
+ IS_SEARCHING,
+ IS_NOT_FOCUSED,
+ IS_FOCUSED,
+ SEARCH_SHORTCUTS_MIN_CHARACTERS,
+} from '~/super_sidebar/components/global_search/constants';
+import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
+import { ENTER_KEY } from '~/lib/utils/keys';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { truncate } from '~/lib/utils/text_utility';
+import {
+ MOCK_SEARCH,
+ MOCK_SEARCH_QUERY,
+ MOCK_USERNAME,
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SEARCH_CONTEXT_FULL,
+} from '../mock_data';
+
+Vue.use(Vuex);
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+describe('HeaderSearchApp', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setSearch: jest.fn(),
+ fetchAutocompleteOptions: jest.fn(),
+ clearAutocomplete: jest.fn(),
+ };
+
+ const createComponent = (initialState, mockGetters) => {
+ const store = new Vuex.Store({
+ state: {
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: {
+ searchQuery: () => MOCK_SEARCH_QUERY,
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMountExtended(HeaderSearchApp, {
+ store,
+ });
+ };
+
+ const formatScopeName = (scopeName) => {
+ if (!scopeName) {
+ return false;
+ }
+ const searchResultsScope = s__('GlobalSearch|in %{scope}');
+ return truncate(
+ sprintf(searchResultsScope, {
+ scope: scopeName,
+ }),
+ SCOPE_TOKEN_MAX_LENGTH,
+ );
+ };
+
+ const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form');
+ const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findScopeToken = () => wrapper.findComponent(GlToken);
+ const findHeaderSearchInputKBD = () => wrapper.find('.keyboard-shortcut-helper');
+ const findHeaderSearchDropdown = () => wrapper.findByTestId('header-search-dropdown-menu');
+ const findHeaderSearchDefaultItems = () => wrapper.findComponent(HeaderSearchDefaultItems);
+ const findHeaderSearchScopedItems = () => wrapper.findComponent(HeaderSearchScopedItems);
+ const findHeaderSearchAutocompleteItems = () =>
+ wrapper.findComponent(HeaderSearchAutocompleteItems);
+ const findDropdownKeyboardNavigation = () => wrapper.findComponent(DropdownKeyboardNavigation);
+ const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
+ const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
+
+ describe('template', () => {
+ describe('always renders', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Header Search Input', () => {
+ expect(findHeaderSearchInput().exists()).toBe(true);
+ });
+
+ it('Header Search Input KBD hint', () => {
+ expect(findHeaderSearchInputKBD().exists()).toBe(true);
+ expect(findHeaderSearchInputKBD().text()).toContain('/');
+ expect(findHeaderSearchInputKBD().attributes('title')).toContain(
+ 'Use the shortcut key <kbd>/</kbd> to start a search',
+ );
+ });
+
+ it('Search Input Description', () => {
+ expect(findSearchInputDescription().exists()).toBe(true);
+ });
+
+ it('Search Results Description', () => {
+ expect(findSearchResultsDescription().exists()).toBe(true);
+ });
+ });
+
+ describe.each`
+ showDropdown | username | showSearchDropdown
+ ${false} | ${null} | ${false}
+ ${false} | ${MOCK_USERNAME} | ${false}
+ ${true} | ${null} | ${false}
+ ${true} | ${MOCK_USERNAME} | ${true}
+ `('Header Search Dropdown', ({ showDropdown, username, showSearchDropdown }) => {
+ describe(`when showDropdown is ${showDropdown} and current_username is ${username}`, () => {
+ beforeEach(() => {
+ window.gon.current_username = username;
+ createComponent();
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ });
+
+ it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(showSearchDropdown);
+ });
+ });
+ });
+
+ describe.each`
+ search | showDefault | showScoped | showAutocomplete
+ ${null} | ${true} | ${false} | ${false}
+ ${''} | ${true} | ${false} | ${false}
+ ${'t'} | ${false} | ${false} | ${true}
+ ${'te'} | ${false} | ${false} | ${true}
+ ${'tes'} | ${false} | ${true} | ${true}
+ ${MOCK_SEARCH} | ${false} | ${true} | ${true}
+ `('Header Search Dropdown Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
+ describe(`when search is ${search}`, () => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search }, {});
+ findHeaderSearchInput().vm.$emit('click');
+ });
+
+ it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => {
+ expect(findHeaderSearchDefaultItems().exists()).toBe(showDefault);
+ });
+
+ it(`should${showScoped ? '' : ' not'} render the Scoped Dropdown Items`, () => {
+ expect(findHeaderSearchScopedItems().exists()).toBe(showScoped);
+ });
+
+ it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Dropdown Items`, () => {
+ expect(findHeaderSearchAutocompleteItems().exists()).toBe(showAutocomplete);
+ });
+
+ it(`should render the Dropdown Navigation Component`, () => {
+ expect(findDropdownKeyboardNavigation().exists()).toBe(true);
+ });
+
+ it(`should close the dropdown when press escape key`, async () => {
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
+ await nextTick();
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ expect(wrapper.emitted().expandSearchBar.length).toBe(1);
+ });
+ });
+ });
+
+ describe.each`
+ username | showDropdown | expectedDesc
+ ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
+ ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
+ ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
+ ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
+ `('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
+ describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
+ beforeEach(() => {
+ window.gon.current_username = username;
+ createComponent();
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ });
+
+ it(`sets description to ${expectedDesc}`, () => {
+ expect(findSearchInputDescription().text()).toBe(expectedDesc);
+ });
+ });
+ });
+
+ describe.each`
+ username | showDropdown | search | loading | searchOptions | expectedDesc
+ ${null} | ${true} | ${''} | ${false} | ${[]} | ${''}
+ ${MOCK_USERNAME} | ${false} | ${''} | ${false} | ${[]} | ${''}
+ ${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
+ ${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
+ ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
+ ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING}
+ `(
+ 'Search Results Description',
+ ({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
+ describe(`search is "${search}", loading is ${loading}, and showSearchDropdown is ${showDropdown}`, () => {
+ beforeEach(() => {
+ window.gon.current_username = username;
+ createComponent(
+ {
+ search,
+ loading,
+ },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ });
+
+ it(`sets description to ${expectedDesc}`, () => {
+ expect(findSearchResultsDescription().text()).toBe(expectedDesc);
+ });
+ });
+ },
+ );
+
+ describe('input box', () => {
+ describe.each`
+ search | searchOptions | hasToken
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true}
+ ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false}
+ ${'x'} | ${[]} | ${false}
+ `('token', ({ search, searchOptions, hasToken }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent(
+ { search },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findHeaderSearchInput().vm.$emit('click');
+ });
+
+ it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
+ searchOptions[0]?.html_id
+ }"`, () => {
+ expect(findScopeToken().exists()).toBe(hasToken);
+ });
+
+ it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${
+ searchOptions[0]?.scope || searchOptions[0]?.description
+ }"`, () => {
+ expect(findScopeToken().exists() && findScopeToken().text()).toBe(
+ formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description),
+ );
+ });
+ });
+ });
+
+ describe('form', () => {
+ describe.each`
+ searchContext | search | searchOptions | isFocused
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false}
+ ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${null} | ${null} | ${[]} | ${true}
+ `('wrapper', ({ searchContext, search, searchOptions, isFocused }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
+ if (isFocused) {
+ findHeaderSearchInput().vm.$emit('click');
+ }
+ });
+
+ const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
+
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
+ if (isSearching) {
+ expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING);
+ return;
+ }
+ if (!isSearching) {
+ expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING);
+ }
+ });
+
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${
+ isFocused ? IS_FOCUSED : IS_NOT_FOCUSED
+ }"`, () => {
+ expect(findHeaderSearchForm().classes()).toContain(
+ isFocused ? IS_FOCUSED : IS_NOT_FOCUSED,
+ );
+ });
+ });
+ });
+
+ describe.each`
+ search | searchOptions | hasIcon | iconName
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false}
+ `('token', ({ search, searchOptions, hasIcon, iconName }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent(
+ { search },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findHeaderSearchInput().vm.$emit('click');
+ });
+
+ it(`icon for data set type "${searchOptions[0]?.html_id}" ${
+ hasIcon ? 'is' : 'is NOT'
+ } rendered`, () => {
+ expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon);
+ });
+
+ it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${
+ searchOptions[0]?.html_id
+ }"`, () => {
+ expect(
+ findScopeToken().findComponent(GlIcon).exists() &&
+ findScopeToken().findComponent(GlIcon).attributes('name'),
+ ).toBe(iconName);
+ });
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent();
+ });
+
+ describe('Header Search Input', () => {
+ describe('when dropdown is closed', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ it('onFocus opens dropdown and triggers snowplow event', async () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ findHeaderSearchInput().vm.$emit('focus');
+
+ await nextTick();
+
+ expect(findHeaderSearchDropdown().exists()).toBe(true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'navigation_top',
+ });
+ });
+
+ it('onClick opens dropdown and triggers snowplow event', async () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ findHeaderSearchInput().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findHeaderSearchDropdown().exists()).toBe(true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'navigation_top',
+ });
+ });
+
+ it('onClick followed by onFocus only triggers a single snowplow event', async () => {
+ findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focus');
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('onInput', () => {
+ describe('when search has text', () => {
+ beforeEach(() => {
+ findHeaderSearchInput().vm.$emit('input', MOCK_SEARCH);
+ });
+
+ it('calls setSearch with search term', () => {
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH);
+ });
+
+ it('calls fetchAutocompleteOptions', () => {
+ expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled();
+ });
+
+ it('does not call clearAutocomplete', () => {
+ expect(actionSpies.clearAutocomplete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when search is emptied', () => {
+ beforeEach(() => {
+ findHeaderSearchInput().vm.$emit('input', '');
+ });
+
+ it('calls setSearch with empty term', () => {
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), '');
+ });
+
+ it('does not call fetchAutocompleteOptions', () => {
+ expect(actionSpies.fetchAutocompleteOptions).not.toHaveBeenCalled();
+ });
+
+ it('calls clearAutocomplete', () => {
+ expect(actionSpies.clearAutocomplete).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('Dropdown Keyboard Navigation', () => {
+ beforeEach(() => {
+ findHeaderSearchInput().vm.$emit('click');
+ });
+
+ it('closes dropdown when @tab is emitted', async () => {
+ expect(findHeaderSearchDropdown().exists()).toBe(true);
+ findDropdownKeyboardNavigation().vm.$emit('tab');
+
+ await nextTick();
+
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe.each`
+ MOCK_INDEX | search
+ ${1} | ${null}
+ ${SEARCH_BOX_INDEX} | ${'test'}
+ ${2} | ${'test1'}
+ `('currentFocusedOption', ({ MOCK_INDEX, search }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search });
+ findHeaderSearchInput().vm.$emit('click');
+ });
+
+ it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => {
+ findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
+ expect(wrapper.vm.currentFocusedOption).toBe(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX]);
+ });
+ });
+ });
+
+ describe('Submitting a search', () => {
+ describe('with no currentFocusedOption', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('onKey-enter submits a search', () => {
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+ });
+
+ describe('with less than min characters and no dropdown results', () => {
+ beforeEach(() => {
+ createComponent({ search: 'x' });
+ });
+
+ it('onKey-enter will NOT submit a search', () => {
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+ });
+
+ describe('with currentFocusedOption', () => {
+ const MOCK_INDEX = 1;
+
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent();
+ findHeaderSearchInput().vm.$emit('click');
+ });
+
+ it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => {
+ findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
+
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_DEFAULT_SEARCH_OPTIONS[MOCK_INDEX].url);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
new file mode 100644
index 00000000000..58e578e4c4c
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -0,0 +1,404 @@
+import {
+ ICON_PROJECT,
+ ICON_GROUP,
+ ICON_SUBGROUP,
+} from '~/super_sidebar/components/global_search/constants';
+import {
+ PROJECTS_CATEGORY,
+ GROUPS_CATEGORY,
+ MSG_ISSUES_ASSIGNED_TO_ME,
+ MSG_ISSUES_IVE_CREATED,
+ MSG_MR_ASSIGNED_TO_ME,
+ MSG_MR_IM_REVIEWER,
+ MSG_MR_IVE_CREATED,
+ MSG_IN_ALL_GITLAB,
+} from '~/vue_shared/global_search/constants';
+
+export const MOCK_USERNAME = 'anyone';
+
+export const MOCK_SEARCH_PATH = '/search';
+
+export const MOCK_ISSUE_PATH = '/dashboard/issues';
+
+export const MOCK_MR_PATH = '/dashboard/merge_requests';
+
+export const MOCK_ALL_PATH = '/';
+
+export const MOCK_AUTOCOMPLETE_PATH = '/autocomplete';
+
+export const MOCK_PROJECT = {
+ id: 123,
+ name: 'MockProject',
+ path: '/mock-project',
+};
+
+export const MOCK_PROJECT_LONG = {
+ id: 124,
+ name: 'Mock Project Name That Is Ridiculously Long And It Goes Forever',
+ path: '/mock-project-name-that-is-ridiculously-long-and-it-goes-forever',
+};
+
+export const MOCK_GROUP = {
+ id: 321,
+ name: 'MockGroup',
+ path: '/mock-group',
+};
+
+export const MOCK_SUBGROUP = {
+ id: 322,
+ name: 'MockSubGroup',
+ path: `${MOCK_GROUP}/mock-subgroup`,
+};
+
+export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test';
+
+export const MOCK_SEARCH = 'test';
+
+export const MOCK_SEARCH_CONTEXT = {
+ project: null,
+ project_metadata: {},
+ group: null,
+ group_metadata: {},
+};
+
+export const MOCK_SEARCH_CONTEXT_FULL = {
+ group: {
+ id: 31,
+ name: 'testGroup',
+ full_name: 'testGroup',
+ },
+ group_metadata: {
+ group_path: 'testGroup',
+ name: 'testGroup',
+ issues_path: '/groups/testGroup/-/issues',
+ mr_path: '/groups/testGroup/-/merge_requests',
+ },
+};
+
+export const MOCK_DEFAULT_SEARCH_OPTIONS = [
+ {
+ html_id: 'default-issues-assigned',
+ title: MSG_ISSUES_ASSIGNED_TO_ME,
+ url: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ },
+ {
+ html_id: 'default-issues-created',
+ title: MSG_ISSUES_IVE_CREATED,
+ url: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
+ },
+ {
+ html_id: 'default-mrs-assigned',
+ title: MSG_MR_ASSIGNED_TO_ME,
+ url: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ },
+ {
+ html_id: 'default-mrs-reviewer',
+ title: MSG_MR_IM_REVIEWER,
+ url: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
+ },
+ {
+ html_id: 'default-mrs-created',
+ title: MSG_MR_IVE_CREATED,
+ url: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
+ },
+];
+
+export const MOCK_SCOPED_SEARCH_OPTIONS = [
+ {
+ html_id: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT.path,
+ },
+ {
+ html_id: 'scoped-in-project-long',
+ scope: MOCK_PROJECT_LONG.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT_LONG.path,
+ },
+ {
+ html_id: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ url: MOCK_GROUP.path,
+ },
+ {
+ html_id: 'scoped-in-subgroup',
+ scope: MOCK_SUBGROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_SUBGROUP,
+ url: MOCK_SUBGROUP.path,
+ },
+ {
+ html_id: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ url: MOCK_ALL_PATH,
+ },
+];
+
+export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
+ {
+ html_id: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT.path,
+ },
+ {
+ html_id: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ url: MOCK_GROUP.path,
+ },
+ {
+ html_id: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ url: MOCK_ALL_PATH,
+ },
+];
+
+export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
+ {
+ category: 'Projects',
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ url: 'project/1',
+ },
+ {
+ category: 'Groups',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ url: 'group/1',
+ },
+ {
+ category: 'Projects',
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
+ url: 'project/2',
+ },
+ {
+ category: 'Help',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+];
+
+export const MOCK_AUTOCOMPLETE_OPTIONS = [
+ {
+ category: 'Projects',
+ html_id: 'autocomplete-Projects-0',
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ url: 'project/1',
+ },
+ {
+ category: 'Groups',
+ html_id: 'autocomplete-Groups-1',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ url: 'group/1',
+ },
+ {
+ category: 'Projects',
+ html_id: 'autocomplete-Projects-2',
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
+ url: 'project/2',
+ },
+ {
+ category: 'Help',
+ html_id: 'autocomplete-Help-3',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+];
+
+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: [
+ {
+ category: 'Projects',
+ html_id: 'autocomplete-Projects-0',
+
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ url: 'project/1',
+ },
+ {
+ category: 'Projects',
+ html_id: 'autocomplete-Projects-2',
+
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
+ url: 'project/2',
+ },
+ ],
+ },
+ {
+ category: 'Help',
+ data: [
+ {
+ category: 'Help',
+ html_id: 'autocomplete-Help-3',
+
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+ ],
+ },
+];
+
+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,
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ url: 'project/1',
+ },
+ {
+ category: 'Projects',
+ html_id: 'autocomplete-Projects-2',
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
+ url: 'project/2',
+ },
+ {
+ category: 'Help',
+ html_id: 'autocomplete-Help-3',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP = [
+ {
+ category: 'Help',
+ data: [
+ {
+ html_id: 'autocomplete-Help-1',
+ category: 'Help',
+ label: 'Rake Tasks Help',
+ url: '/help/raketasks/index',
+ },
+ {
+ html_id: 'autocomplete-Help-2',
+ category: 'Help',
+ label: 'System Hooks Help',
+ url: '/help/system_hooks/system_hooks',
+ },
+ ],
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP = [
+ {
+ category: 'Settings',
+ data: [
+ {
+ html_id: 'autocomplete-Settings-0',
+ category: 'Settings',
+ label: 'User settings',
+ url: '/-/profile',
+ },
+ {
+ html_id: 'autocomplete-Settings-3',
+ category: 'Settings',
+ label: 'Admin Section',
+ url: '/admin',
+ },
+ ],
+ },
+ {
+ category: 'Help',
+ data: [
+ {
+ html_id: 'autocomplete-Help-1',
+ category: 'Help',
+ label: 'Rake Tasks Help',
+ url: '/help/raketasks/index',
+ },
+ {
+ html_id: 'autocomplete-Help-2',
+ category: 'Help',
+ label: 'System Hooks Help',
+ url: '/help/system_hooks/system_hooks',
+ },
+ ],
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2 = [
+ {
+ category: 'Groups',
+ data: [
+ {
+ html_id: 'autocomplete-Groups-0',
+ category: 'Groups',
+ id: 148,
+ label: 'Jashkenas / Test Subgroup / test-subgroup',
+ url: '/jashkenas/test-subgroup/test-subgroup',
+ avatar_url: '',
+ },
+ {
+ html_id: 'autocomplete-Groups-1',
+ category: 'Groups',
+ id: 147,
+ label: 'Jashkenas / Test Subgroup',
+ url: '/jashkenas/test-subgroup',
+ avatar_url: '',
+ },
+ ],
+ },
+ {
+ category: 'Projects',
+ data: [
+ {
+ html_id: 'autocomplete-Projects-2',
+ category: 'Projects',
+ id: 1,
+ value: 'Gitlab Test',
+ label: 'Gitlab Org / Gitlab Test',
+ url: '/gitlab-org/gitlab-test',
+ avatar_url: '/uploads/-/system/project/avatar/1/icons8-gitlab-512.png',
+ },
+ ],
+ },
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
new file mode 100644
index 00000000000..c87b4513309
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
@@ -0,0 +1,113 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import * as actions from '~/super_sidebar/components/global_search/store/actions';
+import * as types from '~/super_sidebar/components/global_search/store/mutation_types';
+import initState from '~/super_sidebar/components/global_search/store/state';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+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('~/alert');
+
+describe('Header Search Store Actions', () => {
+ let state;
+ let mock;
+
+ 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;
+ mock.restore();
+ });
+
+ describe.each`
+ axiosMock | type | expectedMutations
+ ${{ method: 'onGet', code: HTTP_STATUS_OK, 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: HTTP_STATUS_INTERNAL_SERVER_ERROR, 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(() => {
+ state = createState({});
+ mock = new MockAdapter(axios);
+ mock[axiosMock.method]().reply(axiosMock.code, axiosMock.res);
+ });
+ it(`should dispatch the correct mutations`, () => {
+ return testAction({
+ action: actions.fetchAutocompleteOptions,
+ state,
+ expectedMutations,
+ });
+ });
+ });
+ });
+
+ 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,
+ state,
+ expectedMutations: [{ type: types.CLEAR_AUTOCOMPLETE }],
+ });
+ });
+ });
+
+ describe('setSearch', () => {
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ it('calls the SET_SEARCH mutation', () => {
+ return testAction({
+ action: actions.setSearch,
+ payload: MOCK_SEARCH,
+ state,
+ expectedMutations: [{ type: types.SET_SEARCH, payload: MOCK_SEARCH }],
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
new file mode 100644
index 00000000000..dca96da01a7
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
@@ -0,0 +1,333 @@
+import * as getters from '~/super_sidebar/components/global_search/store/getters';
+import initState from '~/super_sidebar/components/global_search/store/state';
+import {
+ MOCK_USERNAME,
+ MOCK_SEARCH_PATH,
+ MOCK_ISSUE_PATH,
+ MOCK_MR_PATH,
+ MOCK_AUTOCOMPLETE_PATH,
+ MOCK_SEARCH_CONTEXT,
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS_DEF,
+ MOCK_PROJECT,
+ MOCK_GROUP,
+ MOCK_ALL_PATH,
+ MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
+
+describe('Header Search Store Getters', () => {
+ let state;
+
+ const createState = (initialState) => {
+ state = 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;
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('searchQuery', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.searchQuery(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'}
+ ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ issues_path: 'project/path' }} | ${'project/path'}
+ `('scopedIssuesPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ group_metadata,
+ project,
+ project_metadata,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.scopedIssuesPath(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | group_metadata | project | project_metadata | expectedPath
+ ${null} | ${null} | ${null} | ${null} | ${MOCK_MR_PATH}
+ ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
+ ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${'project/path'}
+ `('scopedMRPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ group_metadata,
+ project,
+ project_metadata,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.scopedMRPath(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('projectUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.projectUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('groupUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.groupUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('allUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.allUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe('defaultSearchOptions', () => {
+ const mockGetters = {
+ scopedIssuesPath: MOCK_ISSUE_PATH,
+ scopedMRPath: MOCK_MR_PATH,
+ };
+
+ beforeEach(() => {
+ createState();
+ window.gon.current_username = MOCK_USERNAME;
+ });
+
+ it('returns the correct array', () => {
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ );
+ });
+
+ it('returns the correct array if issues path is false', () => {
+ mockGetters.scopedIssuesPath = undefined;
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length),
+ );
+ });
+ });
+
+ describe('scopedSearchOptions', () => {
+ const mockGetters = {
+ projectUrl: MOCK_PROJECT.path,
+ groupUrl: MOCK_GROUP.path,
+ allUrl: MOCK_ALL_PATH,
+ };
+
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ project: MOCK_PROJECT,
+ group: MOCK_GROUP,
+ },
+ });
+ });
+
+ it('returns the correct array', () => {
+ expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_SCOPED_SEARCH_OPTIONS_DEF,
+ );
+ });
+ });
+
+ describe('autocompleteGroupedSearchOptions', () => {
+ beforeEach(() => {
+ createState();
+ state.autocompleteOptions = MOCK_AUTOCOMPLETE_OPTIONS;
+ });
+
+ it('returns the correct grouped array', () => {
+ expect(getters.autocompleteGroupedSearchOptions(state)).toStrictEqual(
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ );
+ });
+ });
+
+ describe.each`
+ search | defaultSearchOptions | scopedSearchOptions | autocompleteGroupedSearchOptions | expectedArray
+ ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS}
+ ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
+ ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)}
+ ${1} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]}
+ ${'('} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]}
+ ${'t'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
+ ${'te'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
+ ${'tes'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)}
+ `(
+ 'searchOptions',
+ ({
+ search,
+ defaultSearchOptions,
+ scopedSearchOptions,
+ autocompleteGroupedSearchOptions,
+ expectedArray,
+ }) => {
+ describe(`when search is ${search} and the defaultSearchOptions${
+ defaultSearchOptions.length ? '' : ' do not'
+ } exist, scopedSearchOptions${
+ scopedSearchOptions.length ? '' : ' do not'
+ } exist, and autocompleteGroupedSearchOptions${
+ autocompleteGroupedSearchOptions.length ? '' : ' do not'
+ } exist`, () => {
+ const mockGetters = {
+ defaultSearchOptions,
+ scopedSearchOptions,
+ autocompleteGroupedSearchOptions,
+ };
+
+ beforeEach(() => {
+ createState();
+ state.search = search;
+ });
+
+ it(`should return the correct combined array`, () => {
+ expect(getters.searchOptions(state, mockGetters)).toStrictEqual(expectedArray);
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
new file mode 100644
index 00000000000..d2dc484e825
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
@@ -0,0 +1,63 @@
+import * as types from '~/super_sidebar/components/global_search/store/mutation_types';
+import mutations from '~/super_sidebar/components/global_search/store/mutations';
+import createState from '~/super_sidebar/components/global_search/store/state';
+import {
+ MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS_RES,
+ MOCK_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
+
+describe('Header Search Store Mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ describe('REQUEST_AUTOCOMPLETE', () => {
+ it('sets loading to true and empties autocompleteOptions array', () => {
+ mutations[types.REQUEST_AUTOCOMPLETE](state);
+
+ expect(state.loading).toBe(true);
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ expect(state.autocompleteError).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_AUTOCOMPLETE_SUCCESS', () => {
+ it('sets loading to false and then formats and sets the autocompleteOptions array', () => {
+ mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS_RES);
+
+ expect(state.loading).toBe(false);
+ expect(state.autocompleteOptions).toStrictEqual(MOCK_AUTOCOMPLETE_OPTIONS);
+ expect(state.autocompleteError).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_AUTOCOMPLETE_ERROR', () => {
+ it('sets loading to false and empties autocompleteOptions array', () => {
+ mutations[types.RECEIVE_AUTOCOMPLETE_ERROR](state);
+
+ expect(state.loading).toBe(false);
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ expect(state.autocompleteError).toBe(true);
+ });
+ });
+
+ describe('CLEAR_AUTOCOMPLETE', () => {
+ it('empties autocompleteOptions array', () => {
+ mutations[types.CLEAR_AUTOCOMPLETE](state);
+
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ expect(state.autocompleteError).toBe(false);
+ });
+ });
+
+ describe('SET_SEARCH', () => {
+ it('sets search to value', () => {
+ mutations[types.SET_SEARCH](state, MOCK_SEARCH);
+
+ expect(state.search).toBe(MOCK_SEARCH);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
new file mode 100644
index 00000000000..6aee895f611
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/groups_list_spec.js
@@ -0,0 +1,87 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import GroupsList from '~/super_sidebar/components/groups_list.vue';
+import SearchResults from '~/super_sidebar/components/search_results.vue';
+import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants';
+
+const username = 'root';
+const viewAllLink = '/path/to/groups';
+const storageKey = `${username}/frequent-groups`;
+
+describe('GroupsList component', () => {
+ let wrapper;
+
+ const findSearchResults = () => wrapper.findComponent(SearchResults);
+ const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList);
+ const findViewAllLink = () => wrapper.findComponent(NavItem);
+
+ const itRendersViewAllItem = () => {
+ it('renders the "View all..." item', () => {
+ expect(findViewAllLink().props('item')).toEqual({
+ icon: 'group',
+ link: viewAllLink,
+ title: s__('Navigation|View all groups'),
+ });
+ });
+ };
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(GroupsList, {
+ propsData: {
+ username,
+ viewAllLink,
+ ...props,
+ },
+ });
+ };
+
+ describe('when displaying search results', () => {
+ const searchResults = ['A search result'];
+
+ beforeEach(() => {
+ createWrapper({
+ isSearch: true,
+ searchResults,
+ });
+ });
+
+ it('renders the search results component', () => {
+ expect(findSearchResults().exists()).toBe(true);
+ expect(findFrequentItemsList().exists()).toBe(false);
+ });
+
+ it('passes the correct props to the search results component', () => {
+ expect(findSearchResults().props()).toEqual({
+ title: s__('Navigation|Groups'),
+ noResultsText: s__('Navigation|No group matches found'),
+ searchResults,
+ });
+ });
+
+ itRendersViewAllItem();
+ });
+
+ describe('when displaying frequent groups', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the frequent items list', () => {
+ expect(findFrequentItemsList().exists()).toBe(true);
+ expect(findSearchResults().exists()).toBe(false);
+ });
+
+ it('passes the correct props to the frequent items list', () => {
+ expect(findFrequentItemsList().props()).toEqual({
+ title: s__('Navigation|Frequent groups'),
+ storageKey,
+ maxItems: MAX_FREQUENT_GROUPS_COUNT,
+ pristineText: s__('Navigation|Groups you visit often will appear here.'),
+ });
+ });
+
+ itRendersViewAllItem();
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index bc847a3e159..1d072c0ba3c 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -68,18 +68,23 @@ describe('HelpCenter component', () => {
});
describe('showKeyboardShortcuts', () => {
+ let button;
+
beforeEach(() => {
jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
- window.toggleShortcutsHelp = jest.fn();
- findButton('Keyboard shortcuts ?').click();
+
+ button = findButton('Keyboard shortcuts ?');
});
it('closes the dropdown', () => {
+ button.click();
expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
});
it('shows the keyboard shortcuts modal', () => {
- expect(window.toggleShortcutsHelp).toHaveBeenCalled();
+ // This relies on the event delegation set up by the Shortcuts class in
+ // ~/behaviors/shortcuts/shortcuts.js.
+ expect(button.classList.contains('js-shortcuts-modal-trigger')).toBe(true);
});
});
diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js
new file mode 100644
index 00000000000..8e00984f500
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/items_list_spec.js
@@ -0,0 +1,63 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ItemsList from '~/super_sidebar/components/items_list.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { cachedFrequentProjects } from '../mock_data';
+
+const mockItems = JSON.parse(cachedFrequentProjects);
+const [firstMockedProject] = mockItems;
+
+describe('ItemsList component', () => {
+ let wrapper;
+
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
+
+ const createWrapper = ({ props = {}, slots = {} } = {}) => {
+ wrapper = shallowMountExtended(ItemsList, {
+ propsData: {
+ ...props,
+ },
+ slots,
+ });
+ };
+
+ it('does not render nav items when there are no items', () => {
+ createWrapper();
+
+ expect(findNavItems().length).toBe(0);
+ });
+
+ it('renders one nav item per item', () => {
+ createWrapper({
+ props: {
+ items: mockItems,
+ },
+ });
+
+ expect(findNavItems().length).not.toBe(0);
+ expect(findNavItems().length).toBe(mockItems.length);
+ });
+
+ it('passes the correct props to the nav items', () => {
+ createWrapper({
+ props: {
+ items: mockItems,
+ },
+ });
+ const firstNavItem = findNavItems().at(0);
+
+ expect(firstNavItem.props('item')).toEqual(firstMockedProject);
+ });
+
+ it('renders the `view-all-items` slot', () => {
+ const testId = 'view-all-items';
+ createWrapper({
+ slots: {
+ 'view-all-items': {
+ template: `<div data-testid="${testId}" />`,
+ },
+ },
+ });
+
+ expect(wrapper.findByTestId(testId).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
new file mode 100644
index 00000000000..22989c1a5f9
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -0,0 +1,49 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+
+describe('NavItem component', () => {
+ let wrapper;
+
+ const findLink = () => wrapper.findByTestId('nav-item-link');
+ const findPill = () => wrapper.findComponent(GlBadge);
+ const createWrapper = (item, props = {}) => {
+ wrapper = shallowMountExtended(NavItem, {
+ propsData: {
+ item,
+ ...props,
+ },
+ });
+ };
+
+ describe('pills', () => {
+ it.each([0, 5, 3.4, 'foo', '10%'])('item with pill_data `%p` renders a pill', (pillCount) => {
+ createWrapper({ title: 'Foo', pill_count: pillCount });
+
+ expect(findPill().text()).toEqual(pillCount.toString());
+ });
+
+ it.each([null, undefined, false, true, '', NaN, Number.POSITIVE_INFINITY])(
+ 'item with pill_data `%p` renders no pill',
+ (pillCount) => {
+ createWrapper({ title: 'Foo', pill_count: pillCount });
+
+ expect(findPill().exists()).toEqual(false);
+ },
+ );
+ });
+
+ it('applies custom link classes', () => {
+ const customClass = 'customClass';
+ createWrapper(
+ { title: 'Foo' },
+ {
+ linkClasses: {
+ [customClass]: true,
+ },
+ },
+ );
+
+ expect(findLink().attributes('class')).toContain(customClass);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
new file mode 100644
index 00000000000..cdc003b14e0
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/projects_list_spec.js
@@ -0,0 +1,82 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import ProjectsList from '~/super_sidebar/components/projects_list.vue';
+import SearchResults from '~/super_sidebar/components/search_results.vue';
+import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants';
+
+const username = 'root';
+const viewAllLink = '/path/to/projects';
+const storageKey = `${username}/frequent-projects`;
+
+describe('ProjectsList component', () => {
+ let wrapper;
+
+ const findSearchResults = () => wrapper.findComponent(SearchResults);
+ const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList);
+ const findViewAllLink = () => wrapper.findComponent(NavItem);
+
+ const itRendersViewAllItem = () => {
+ it('renders the "View all..." item', () => {
+ expect(findViewAllLink().props('item')).toEqual({
+ icon: 'project',
+ link: viewAllLink,
+ title: s__('Navigation|View all projects'),
+ });
+ });
+ };
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(ProjectsList, {
+ propsData: {
+ username,
+ viewAllLink,
+ ...props,
+ },
+ });
+ };
+
+ describe('when displaying search results', () => {
+ const searchResults = ['A search result'];
+
+ beforeEach(() => {
+ createWrapper({
+ isSearch: true,
+ searchResults,
+ });
+ });
+
+ it('renders the search results component', () => {
+ expect(findSearchResults().exists()).toBe(true);
+ expect(findFrequentItemsList().exists()).toBe(false);
+ });
+
+ it('passes the correct props to the search results component', () => {
+ expect(findSearchResults().props()).toEqual({
+ title: s__('Navigation|Projects'),
+ noResultsText: s__('Navigation|No project matches found'),
+ searchResults,
+ });
+ });
+
+ itRendersViewAllItem();
+ });
+
+ describe('when displaying frequent projects', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes the correct props to the frequent items list', () => {
+ expect(findFrequentItemsList().props()).toEqual({
+ title: s__('Navigation|Frequent projects'),
+ storageKey,
+ maxItems: MAX_FREQUENT_PROJECTS_COUNT,
+ pristineText: s__('Navigation|Projects you visit often will appear here.'),
+ });
+ });
+
+ itRendersViewAllItem();
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js
new file mode 100644
index 00000000000..dd48935c138
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/search_results_spec.js
@@ -0,0 +1,57 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { s__ } from '~/locale';
+import SearchResults from '~/super_sidebar/components/search_results.vue';
+import ItemsList from '~/super_sidebar/components/items_list.vue';
+
+const title = s__('Navigation|PROJECTS');
+const noResultsText = s__('Navigation|No project matches found');
+
+describe('SearchResults component', () => {
+ let wrapper;
+
+ const findListTitle = () => wrapper.findByTestId('list-title');
+ const findItemsList = () => wrapper.findComponent(ItemsList);
+ const findEmptyText = () => wrapper.findByTestId('empty-text');
+
+ const createWrapper = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(SearchResults, {
+ propsData: {
+ title,
+ noResultsText,
+ ...props,
+ },
+ });
+ };
+
+ describe('default state', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it("renders the list's title", () => {
+ expect(findListTitle().text()).toBe(title);
+ });
+
+ it('renders the empty text', () => {
+ expect(findEmptyText().exists()).toBe(true);
+ expect(findEmptyText().text()).toBe(noResultsText);
+ });
+ });
+
+ describe('when displaying search results', () => {
+ it('shows search results', () => {
+ const searchResults = [{ id: 1 }];
+ createWrapper({ props: { isSearch: true, searchResults } });
+
+ expect(findItemsList().props('items')[0]).toEqual(searchResults[0]);
+ });
+
+ it('shows the no results text if search results are empty', () => {
+ const searchResults = [];
+ createWrapper({ props: { isSearch: true, searchResults } });
+
+ expect(findItemsList().props('items').length).toEqual(0);
+ expect(findEmptyText().text()).toBe(noResultsText);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/sidebar_portal_spec.js b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js
new file mode 100644
index 00000000000..3ef1cb7e692
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js
@@ -0,0 +1,68 @@
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
+import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
+
+describe('SidebarPortal', () => {
+ let targetWrapper;
+
+ const Target = {
+ components: { SidebarPortalTarget },
+ props: ['show'],
+ template: '<sidebar-portal-target v-if="show" />',
+ };
+
+ const Source = {
+ components: { SidebarPortal },
+ template: '<sidebar-portal><br data-testid="test"></sidebar-portal>',
+ };
+
+ const mountSource = () => {
+ mount(Source);
+ };
+
+ const mountTarget = ({ show = true } = {}) => {
+ targetWrapper = mount(Target, {
+ propsData: { show },
+ attachTo: document.body,
+ });
+ };
+
+ const findTestContent = () => targetWrapper.find('[data-testid="test"]');
+
+ it('renders content into the target', async () => {
+ mountTarget();
+ await nextTick();
+
+ mountSource();
+ await nextTick();
+
+ expect(findTestContent().exists()).toBe(true);
+ });
+
+ it('waits for target to be available before rendering', async () => {
+ mountSource();
+ await nextTick();
+
+ mountTarget();
+ await nextTick();
+
+ expect(findTestContent().exists()).toBe(true);
+ });
+
+ it('supports conditional rendering of target', async () => {
+ mountTarget({ show: false });
+ await nextTick();
+
+ mountSource();
+ await nextTick();
+
+ expect(findTestContent().exists()).toBe(false);
+
+ await targetWrapper.setProps({ show: true });
+ expect(findTestContent().exists()).toBe(true);
+
+ await targetWrapper.setProps({ show: false });
+ expect(findTestContent().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 45fc30c08f0..32921da23aa 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -2,13 +2,21 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
+import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
+import { isCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import { sidebarData } from '../mock_data';
+jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager', () => ({
+ isCollapsed: jest.fn(),
+}));
+
describe('SuperSidebar component', () => {
let wrapper;
+ const findSidebar = () => wrapper.find('.super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
+ const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
@@ -20,16 +28,33 @@ describe('SuperSidebar component', () => {
};
describe('default', () => {
- beforeEach(() => {
+ it('add aria-hidden and inert attributes when collapsed', () => {
+ isCollapsed.mockReturnValue(true);
createWrapper();
+ expect(findSidebar().attributes('aria-hidden')).toBe('true');
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ });
+
+ it('does not add aria-hidden and inert attributes when expanded', () => {
+ isCollapsed.mockReturnValue(false);
+ createWrapper();
+ expect(findSidebar().attributes('aria-hidden')).toBe('false');
+ expect(findSidebar().attributes('inert')).toBe(undefined);
});
it('renders UserBar with sidebarData', () => {
+ createWrapper();
expect(findUserBar().props('sidebarData')).toBe(sidebarData);
});
it('renders HelpCenter with sidebarData', () => {
+ createWrapper();
expect(findHelpCenter().props('sidebarData')).toBe(sidebarData);
});
+
+ it('renders SidebarPortalTarget', () => {
+ createWrapper();
+ expect(findSidebarPortalTarget().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index eceb792c3db..ae15dd55644 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,3 +1,4 @@
+import { GlBadge } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
@@ -12,12 +13,12 @@ describe('UserBar component', () => {
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
+ const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
- const createWrapper = (props = {}) => {
+ const createWrapper = (extraSidebarData = {}) => {
wrapper = shallowMountExtended(UserBar, {
propsData: {
- sidebarData,
- ...props,
+ sidebarData: { ...sidebarData, ...extraSidebarData },
},
provide: {
rootPath: '/',
@@ -55,5 +56,29 @@ describe('UserBar component', () => {
expect(findCounter(2).props('href')).toBe('/dashboard/todos');
expect(findCounter(2).props('label')).toBe(__('To-Do list'));
});
+
+ it('renders branding logo', () => {
+ expect(findBrandLogo().exists()).toBe(true);
+ expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url);
+ });
+ });
+
+ describe('GitLab Next badge', () => {
+ describe('when on canary', () => {
+ it('should render a badge to switch off GitLab Next', () => {
+ createWrapper({ gitlab_com_and_canary: true });
+ const badge = wrapper.findComponent(GlBadge);
+ expect(badge.text()).toBe('Next');
+ expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url);
+ });
+ });
+
+ describe('when not on canary', () => {
+ it('should not render the GitLab Next badge', () => {
+ createWrapper({ gitlab_com_and_canary: false });
+ const badge = wrapper.findComponent(GlBadge);
+ expect(badge.exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
new file mode 100644
index 00000000000..b6231e03722
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -0,0 +1,375 @@
+import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserMenu from '~/super_sidebar/components/user_menu.vue';
+import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { mockTracking } from 'helpers/tracking_helper';
+import PersistentUserCallout from '~/persistent_user_callout';
+import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
+
+describe('UserMenu component', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const GlEmoji = { template: '<img/>' };
+ const toggleNewNavEndpoint = invalidUrl;
+ const showDropdown = () => wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
+
+ const createWrapper = (userDataChanges = {}) => {
+ wrapper = mountExtended(UserMenu, {
+ propsData: {
+ data: {
+ ...userMenuMockData,
+ ...userDataChanges,
+ },
+ },
+ stubs: {
+ GlEmoji,
+ GlAvatar: true,
+ },
+ provide: {
+ toggleNewNavEndpoint,
+ },
+ });
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ };
+
+ describe('Toggle button', () => {
+ let toggle;
+
+ beforeEach(() => {
+ createWrapper();
+ toggle = wrapper.findByTestId('base-dropdown-toggle');
+ });
+
+ it('renders User Avatar in a toggle', () => {
+ const avatar = toggle.findComponent(GlAvatar);
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ entityName: userMenuMockData.name,
+ src: userMenuMockData.avatar_url,
+ });
+ });
+
+ it('renders screen reader text', () => {
+ expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`);
+ });
+ });
+
+ describe('User Menu Group', () => {
+ it('renders and passes data to it', () => {
+ createWrapper();
+ const userNameGroup = wrapper.findComponent(UserNameGroup);
+ expect(userNameGroup.exists()).toBe(true);
+ expect(userNameGroup.props('user')).toEqual(userMenuMockData);
+ });
+ });
+
+ describe('User status item', () => {
+ let item;
+
+ const setItem = ({ can_update, busy, customized } = {}) => {
+ createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } });
+ item = wrapper.findByTestId('status-item');
+ };
+
+ describe('When user cannot update the status', () => {
+ it('does not render the status menu item', () => {
+ setItem();
+ expect(item.exists()).toBe(false);
+ });
+ });
+
+ describe('When user can update the status', () => {
+ it('renders the status menu item', () => {
+ setItem({ can_update: true });
+ expect(item.exists()).toBe(true);
+ });
+
+ it('should set the CSS class for triggering status update modal', () => {
+ setItem({ can_update: true });
+ expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
+ });
+
+ describe('renders correct label', () => {
+ it.each`
+ busy | customized | label
+ ${false} | ${false} | ${'Set status'}
+ ${false} | ${true} | ${'Edit status'}
+ ${true} | ${false} | ${'Edit status'}
+ ${true} | ${true} | ${'Edit status'}
+ `(
+ 'when busy is "$busy" and customized is "$customized" the label is "$label"',
+ ({ busy, customized, label }) => {
+ setItem({ can_update: true, busy, customized });
+ expect(item.text()).toBe(label);
+ },
+ );
+ });
+
+ describe('Status update modal wrapper', () => {
+ const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper');
+
+ it('renders the modal wrapper', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().exists()).toBe(true);
+ });
+
+ it('sets default data attributes when status is not customized', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
+ });
+
+ it('sets user status as data attributes when status is customized', () => {
+ setItem({ can_update: true, customized: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': userMenuMockStatus.emoji,
+ 'data-current-message': userMenuMockStatus.message,
+ 'data-current-availability': userMenuMockStatus.availability,
+ 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ });
+ });
+ });
+ });
+ });
+
+ describe('Start Ultimate trial item', () => {
+ let item;
+
+ const setItem = ({ has_start_trial } = {}) => {
+ createWrapper({ trial: { has_start_trial } });
+ item = wrapper.findByTestId('start-trial-item');
+ };
+
+ describe('When Ultimate trial is not suggested for the user', () => {
+ it('does not render the start trial menu item', () => {
+ setItem();
+ expect(item.exists()).toBe(false);
+ });
+ });
+
+ describe('When Ultimate trial can be suggested for the user', () => {
+ it('does render the start trial menu item', () => {
+ setItem({ has_start_trial: true });
+ expect(item.exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('Buy Pipeline Minutes item', () => {
+ let item;
+
+ const setItem = ({
+ show_buy_pipeline_minutes,
+ show_with_subtext,
+ show_notification_dot,
+ } = {}) => {
+ createWrapper({
+ pipeline_minutes: {
+ ...userMenuMockPipelineMinutes,
+ show_buy_pipeline_minutes,
+ show_with_subtext,
+ show_notification_dot,
+ },
+ });
+ item = wrapper.findByTestId('buy-pipeline-minutes-item');
+ };
+
+ describe('When does NOT meet the condition to buy CI minutes', () => {
+ beforeEach(() => {
+ setItem();
+ });
+
+ it('does NOT render the buy pipeline minutes item', () => {
+ expect(item.exists()).toBe(false);
+ });
+
+ it('does not track the Sentry event', () => {
+ showDropdown();
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('When does meet the condition to buy CI minutes', () => {
+ it('does render the menu item', () => {
+ setItem({ show_buy_pipeline_minutes: true });
+ expect(item.exists()).toBe(true);
+ });
+
+ it('tracks the Sentry event', () => {
+ setItem({ show_buy_pipeline_minutes: true });
+ showDropdown();
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ userMenuMockPipelineMinutes.tracking_attrs['track-action'],
+ {
+ label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
+ property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
+ },
+ );
+ });
+
+ describe('Callout & notification dot', () => {
+ let spyFactory;
+
+ beforeEach(() => {
+ spyFactory = jest.spyOn(PersistentUserCallout, 'factory');
+ });
+
+ describe('When `show_notification_dot` is `false`', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true, show_notification_dot: false });
+ showDropdown();
+ });
+
+ it('does not set callout attributes', () => {
+ expect(item.attributes()).not.toEqual(
+ expect.objectContaining({
+ 'data-feature-id': userMenuMockPipelineMinutes.callout_attrs.feature_id,
+ 'data-dismiss-endpoint': userMenuMockPipelineMinutes.callout_attrs.dismiss_endpoint,
+ }),
+ );
+ });
+
+ it('does not initialize the Persistent Callout', () => {
+ expect(spyFactory).not.toHaveBeenCalled();
+ });
+
+ it('does not render notification dot', () => {
+ expect(wrapper.findByTestId('buy-pipeline-minutes-notification-dot').exists()).toBe(
+ false,
+ );
+ });
+ });
+
+ describe('When `show_notification_dot` is `true`', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true, show_notification_dot: true });
+ showDropdown();
+ });
+
+ it('sets the callout data attributes', () => {
+ expect(item.attributes()).toEqual(
+ expect.objectContaining({
+ 'data-feature-id': userMenuMockPipelineMinutes.callout_attrs.feature_id,
+ 'data-dismiss-endpoint': userMenuMockPipelineMinutes.callout_attrs.dismiss_endpoint,
+ }),
+ );
+ });
+
+ it('initializes the Persistent Callout', () => {
+ expect(spyFactory).toHaveBeenCalled();
+ });
+
+ it('renders notification dot', () => {
+ expect(wrapper.findByTestId('buy-pipeline-minutes-notification-dot').exists()).toBe(
+ true,
+ );
+ });
+ });
+ });
+
+ describe('Warning message', () => {
+ it('does not display the warning message when `show_with_subtext` is `false`', () => {
+ setItem({ show_buy_pipeline_minutes: true });
+
+ expect(item.text()).not.toContain(UserMenu.i18n.oneOfGroupsRunningOutOfPipelineMinutes);
+ });
+
+ it('displays the text and warning message when `show_with_subtext` is true', () => {
+ setItem({ show_buy_pipeline_minutes: true, show_with_subtext: true });
+
+ expect(item.text()).toContain(UserMenu.i18n.oneOfGroupsRunningOutOfPipelineMinutes);
+ });
+ });
+ });
+ });
+
+ describe('Edit profile item', () => {
+ it('should render a link to the profile page', () => {
+ createWrapper();
+ const item = wrapper.findByTestId('edit-profile-item');
+ expect(item.text()).toBe(UserMenu.i18n.editProfile);
+ expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path);
+ });
+ });
+
+ describe('Preferences item', () => {
+ it('should render a link to the profile page', () => {
+ createWrapper();
+ const item = wrapper.findByTestId('preferences-item');
+ expect(item.text()).toBe(UserMenu.i18n.preferences);
+ expect(item.find('a').attributes('href')).toBe(
+ userMenuMockData.settings.profile_preferences_path,
+ );
+ });
+ });
+
+ describe('GitLab Next item', () => {
+ describe('on gitlab.com', () => {
+ it('should render a link to switch to GitLab Next', () => {
+ createWrapper({ gitlab_com_but_not_canary: true });
+ const item = wrapper.findByTestId('gitlab-next-item');
+ expect(item.text()).toBe(UserMenu.i18n.gitlabNext);
+ expect(item.find('a').attributes('href')).toBe(userMenuMockData.canary_toggle_com_url);
+ });
+ });
+
+ describe('anywhere else', () => {
+ it('should not render the GitLab Next link', () => {
+ createWrapper({ gitlab_com_but_not_canary: false });
+ const item = wrapper.findByTestId('gitlab-next-item');
+ expect(item.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('New navigation toggle item', () => {
+ it('should render menu item with new navigation toggle', () => {
+ createWrapper();
+ const toggleItem = wrapper.findComponent(NewNavToggle);
+ expect(toggleItem.exists()).toBe(true);
+ expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint);
+ });
+ });
+
+ describe('Feedback item', () => {
+ it('should render feedback item with a link to a new GitLab issue', () => {
+ createWrapper();
+ const feedbackItem = wrapper.findByTestId('feedback-item');
+ expect(feedbackItem.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
+ });
+ });
+
+ describe('Sign out group', () => {
+ const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
+
+ it('should not render sign out group when user cannot sign out', () => {
+ createWrapper();
+ expect(findSignOutGroup().exists()).toBe(false);
+ });
+
+ describe('when user can sign out', () => {
+ beforeEach(() => {
+ createWrapper({ can_sign_out: true });
+ });
+
+ it('should render sign out group', () => {
+ expect(findSignOutGroup().exists()).toBe(true);
+ });
+
+ it('should render the menu item with a link to sign out and correct data attribute', () => {
+ expect(findSignOutGroup().find('a').attributes('href')).toBe(
+ userMenuMockData.sign_out_link,
+ );
+ expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
new file mode 100644
index 00000000000..c06c8c218d4
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -0,0 +1,100 @@
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import { userMenuMockData, userMenuMockStatus } from '../mock_data';
+
+describe('UserNameGroup component', () => {
+ let wrapper;
+
+ const findGlDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
+ const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+ const findGlTooltip = () => wrapper.findComponent(GlTooltip);
+ const findUserStatus = () => wrapper.findByTestId('user-menu-status');
+
+ const GlEmoji = { template: '<img/>' };
+
+ const createWrapper = (userDataChanges = {}) => {
+ wrapper = shallowMountExtended(UserNameGroup, {
+ propsData: {
+ user: {
+ ...userMenuMockData,
+ ...userDataChanges,
+ },
+ },
+ stubs: {
+ GlEmoji,
+ GlDisclosureDropdownItem,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the menu item in a separate group', () => {
+ expect(findGlDisclosureDropdownGroup().exists()).toBe(true);
+ });
+
+ it('renders menu item', () => {
+ expect(findGlDisclosureDropdownItem().exists()).toBe(true);
+ });
+
+ it('passes the item to the disclosure dropdown item', () => {
+ expect(findGlDisclosureDropdownItem().props('item')).toEqual({
+ text: userMenuMockData.name,
+ href: userMenuMockData.link_to_profile,
+ });
+ });
+
+ it("renders user's name", () => {
+ expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.name);
+ });
+
+ it("renders user's username", () => {
+ expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.username);
+ });
+
+ describe('Busy status', () => {
+ it('should not render "Busy" when user is NOT busy', () => {
+ expect(findGlDisclosureDropdownItem().text()).not.toContain('Busy');
+ });
+ it('should render "Busy" when user is busy', () => {
+ createWrapper({ status: { customized: true, busy: true } });
+
+ expect(findGlDisclosureDropdownItem().text()).toContain('Busy');
+ });
+ });
+
+ describe('User status', () => {
+ describe('when not customized', () => {
+ it('should not render it', () => {
+ expect(findUserStatus().exists()).toBe(false);
+ });
+ });
+
+ describe('when customized', () => {
+ beforeEach(() => {
+ createWrapper({ status: { ...userMenuMockStatus, customized: true } });
+ });
+
+ it('should render it', () => {
+ expect(findUserStatus().exists()).toBe(true);
+ });
+
+ it('should render status emoji', () => {
+ expect(findUserStatus().findComponent(GlEmoji).attributes('data-name')).toBe(
+ userMenuMockData.status.emoji,
+ );
+ });
+
+ it('should render status message', () => {
+ expect(findUserStatus().text()).toContain(userMenuMockData.status.message);
+ });
+
+ it("sets the tooltip's target to the status container", () => {
+ expect(findGlTooltip().props('target')?.()).toBe(findUserStatus().element);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 91a2dc93a47..b540f85d9fe 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -1,3 +1,5 @@
+import invalidUrl from '~/lib/utils/invalid_url';
+
export const createNewMenuGroups = [
{
name: 'This group',
@@ -58,15 +60,23 @@ export const mergeRequestMenuGroup = [
];
export const sidebarData = {
+ current_menu_items: [],
+ current_context_header: {
+ title: 'Your Work',
+ icon: 'work',
+ },
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img_administrator',
+ logo_url: 'path/to/logo',
assigned_open_issues_count: 1,
todos_pending_count: 3,
issues_dashboard_path: 'path/to/issues',
total_merge_requests_count: 4,
create_new_menu_groups: createNewMenuGroups,
merge_request_menu: mergeRequestMenuGroup,
+ projects_path: 'path/to/projects',
+ groups_path: 'path/to/groups',
support_path: '/support',
display_whats_new: true,
whats_new_most_recent_release_items_count: 5,
@@ -74,4 +84,181 @@ export const sidebarData = {
show_version_check: false,
gitlab_version: { major: 16, minor: 0 },
gitlab_version_check: { severity: 'success' },
+ gitlab_com_and_canary: false,
+ canary_toggle_com_url: 'https://next.gitlab.com',
+};
+
+export const userMenuMockStatus = {
+ can_update: false,
+ busy: false,
+ customized: false,
+ emoji: 'art',
+ message: 'Working on user menu in super sidebar',
+ availability: 'busy',
+ clear_after: '2023-02-09 20:06:35 UTC',
+};
+
+export const userMenuMockPipelineMinutes = {
+ show_buy_pipeline_minutes: false,
+ show_notification_dot: false,
+ callout_attrs: {
+ feature_id: 'pipeline_minutes',
+ dismiss_endpoint: '/-/dismiss',
+ },
+ buy_pipeline_minutes_path: '/buy/pipeline_minutes',
+ tracking_attrs: {
+ 'track-action': 'trackAction',
+ 'track-label': 'label',
+ 'track-property': 'property',
+ },
+};
+
+export const userMenuMockData = {
+ name: 'Orange Fox',
+ username: 'thefox',
+ avatar_url: invalidUrl,
+ has_link_to_profile: true,
+ link_to_profile: '/thefox',
+ status: userMenuMockStatus,
+ trial: {
+ has_start_trial: false,
+ },
+ settings: {
+ profile_path: invalidUrl,
+ profile_preferences_path: invalidUrl,
+ },
+ pipeline_minutes: userMenuMockPipelineMinutes,
+ can_sign_out: false,
+ sign_out_link: invalidUrl,
+ gitlab_com_but_not_canary: true,
+ canary_toggle_com_url: 'https://next.gitlab.com',
+};
+
+export const cachedFrequentProjects = JSON.stringify([
+ {
+ id: 1,
+ name: 'Cached project 1',
+ namespace: 'Cached Namespace 1 / Cached project 1',
+ webUrl: '/cached-namespace-1/cached-project-1',
+ avatarUrl: '/uploads/-/avatar1.png',
+ lastAccessedOn: 1676325329054,
+ frequency: 10,
+ },
+ {
+ id: 2,
+ name: 'Cached project 2',
+ namespace: 'Cached Namespace 2 / Cached project 2',
+ webUrl: '/cached-namespace-2/cached-project-2',
+ avatarUrl: '/uploads/-/avatar2.png',
+ lastAccessedOn: 1674314684308,
+ frequency: 8,
+ },
+ {
+ id: 3,
+ name: 'Cached project 3',
+ namespace: 'Cached Namespace 3 / Cached project 3',
+ webUrl: '/cached-namespace-3/cached-project-3',
+ avatarUrl: '/uploads/-/avatar3.png',
+ lastAccessedOn: 1664977333191,
+ frequency: 12,
+ },
+ {
+ id: 4,
+ name: 'Cached project 4',
+ namespace: 'Cached Namespace 4 / Cached project 4',
+ webUrl: '/cached-namespace-4/cached-project-4',
+ avatarUrl: '/uploads/-/avatar4.png',
+ lastAccessedOn: 1674315407569,
+ frequency: 3,
+ },
+ {
+ id: 5,
+ name: 'Cached project 5',
+ namespace: 'Cached Namespace 5 / Cached project 5',
+ webUrl: '/cached-namespace-5/cached-project-5',
+ avatarUrl: '/uploads/-/avatar5.png',
+ lastAccessedOn: 1677084729436,
+ frequency: 21,
+ },
+ {
+ id: 6,
+ name: 'Cached project 6',
+ namespace: 'Cached Namespace 6 / Cached project 6',
+ webUrl: '/cached-namespace-6/cached-project-6',
+ avatarUrl: '/uploads/-/avatar6.png',
+ lastAccessedOn: 1676325329679,
+ frequency: 5,
+ },
+]);
+
+export const cachedFrequentGroups = JSON.stringify([
+ {
+ id: 1,
+ name: 'Cached group 1',
+ namespace: 'Cached Namespace 1',
+ webUrl: '/cached-namespace-1/cached-group-1',
+ avatarUrl: '/uploads/-/avatar1.png',
+ lastAccessedOn: 1676325329054,
+ frequency: 10,
+ },
+ {
+ id: 2,
+ name: 'Cached group 2',
+ namespace: 'Cached Namespace 2',
+ webUrl: '/cached-namespace-2/cached-group-2',
+ avatarUrl: '/uploads/-/avatar2.png',
+ lastAccessedOn: 1674314684308,
+ frequency: 8,
+ },
+ {
+ id: 3,
+ name: 'Cached group 3',
+ namespace: 'Cached Namespace 3',
+ webUrl: '/cached-namespace-3/cached-group-3',
+ avatarUrl: '/uploads/-/avatar3.png',
+ lastAccessedOn: 1664977333191,
+ frequency: 12,
+ },
+ {
+ id: 4,
+ name: 'Cached group 4',
+ namespace: 'Cached Namespace 4',
+ webUrl: '/cached-namespace-4/cached-group-4',
+ avatarUrl: '/uploads/-/avatar4.png',
+ lastAccessedOn: 1674315407569,
+ frequency: 3,
+ },
+]);
+
+export const searchUserProjectsAndGroupsResponseMock = {
+ data: {
+ projects: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Project/2',
+ name: 'Gitlab Shell',
+ namespace: 'Gitlab Org / Gitlab Shell',
+ webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-shell',
+ avatarUrl: null,
+ __typename: 'Project',
+ },
+ ],
+ },
+
+ user: {
+ id: 'gid://gitlab/User/1',
+ groups: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/60',
+ name: 'GitLab Instance',
+ namespace: 'gitlab-instance-2e4abb29',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-instance-2e4abb29',
+ avatarUrl: null,
+ __typename: 'Group',
+ },
+ ],
+ },
+ },
+ },
};
diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
new file mode 100644
index 00000000000..3824965970b
--- /dev/null
+++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
@@ -0,0 +1,157 @@
+import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
+import { getCookie, setCookie } from '~/lib/utils/common_utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import {
+ SIDEBAR_COLLAPSED_CLASS,
+ SIDEBAR_COLLAPSED_COOKIE,
+ SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
+ toggleSuperSidebarCollapsed,
+ initSuperSidebarCollapsedState,
+ bindSuperSidebarCollapsedEvents,
+ findPage,
+ findSidebar,
+ findToggles,
+} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+
+const { xl, sm } = breakpoints;
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ getCookie: jest.fn(),
+ setCookie: jest.fn(),
+}));
+
+const pageHasCollapsedClass = (hasClass) => {
+ if (hasClass) {
+ expect(findPage().classList).toContain(SIDEBAR_COLLAPSED_CLASS);
+ } else {
+ expect(findPage().classList).not.toContain(SIDEBAR_COLLAPSED_CLASS);
+ }
+};
+
+describe('Super Sidebar Collapsed State Manager', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div class="page-with-super-sidebar">
+ <aside class="super-sidebar"></aside>
+ <button class="js-super-sidebar-toggle"></button>
+ </div>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('toggleSuperSidebarCollapsed', () => {
+ it.each`
+ collapsed | saveCookie | windowWidth | hasClass
+ ${true} | ${true} | ${xl} | ${true}
+ ${true} | ${false} | ${xl} | ${true}
+ ${true} | ${true} | ${sm} | ${true}
+ ${true} | ${false} | ${sm} | ${true}
+ ${false} | ${true} | ${xl} | ${false}
+ ${false} | ${false} | ${xl} | ${false}
+ ${false} | ${true} | ${sm} | ${false}
+ ${false} | ${false} | ${sm} | ${false}
+ `(
+ 'when collapsed is $collapsed, saveCookie is $saveCookie, and windowWidth is $windowWidth then page class contains `page-with-super-sidebar-collapsed` is $hasClass',
+ ({ collapsed, saveCookie, windowWidth, hasClass }) => {
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth);
+
+ toggleSuperSidebarCollapsed(collapsed, saveCookie);
+
+ pageHasCollapsedClass(hasClass);
+ expect(findSidebar().ariaHidden).toBe(collapsed);
+ expect(findSidebar().inert).toBe(collapsed);
+
+ if (saveCookie && windowWidth >= xl) {
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, collapsed, {
+ expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
+ });
+ } else {
+ expect(setCookie).not.toHaveBeenCalled();
+ }
+ },
+ );
+
+ describe('focus', () => {
+ it.each`
+ collapsed | isUserAction
+ ${false} | ${true}
+ ${false} | ${false}
+ ${true} | ${true}
+ ${true} | ${false}
+ `(
+ 'when collapsed is $collapsed, isUserAction is $isUserAction',
+ ({ collapsed, isUserAction }) => {
+ const sidebar = findSidebar();
+ jest.spyOn(sidebar, 'focus');
+ toggleSuperSidebarCollapsed(collapsed, false, isUserAction);
+
+ if (!collapsed && isUserAction) {
+ expect(sidebar.focus).toHaveBeenCalled();
+ } else {
+ expect(sidebar.focus).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+ });
+
+ describe('initSuperSidebarCollapsedState', () => {
+ it.each`
+ windowWidth | cookie | hasClass
+ ${xl} | ${undefined} | ${false}
+ ${sm} | ${undefined} | ${true}
+ ${xl} | ${'true'} | ${true}
+ ${sm} | ${'true'} | ${true}
+ `(
+ 'sets page class to `page-with-super-sidebar-collapsed` when windowWidth is $windowWidth and cookie value is $cookie',
+ ({ windowWidth, cookie, hasClass }) => {
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth);
+ getCookie.mockReturnValue(cookie);
+
+ initSuperSidebarCollapsedState();
+
+ pageHasCollapsedClass(hasClass);
+ expect(setCookie).not.toHaveBeenCalled();
+ },
+ );
+ });
+
+ describe('bindSuperSidebarCollapsedEvents', () => {
+ it.each`
+ windowWidth | cookie | hasClass
+ ${xl} | ${undefined} | ${true}
+ ${sm} | ${undefined} | ${true}
+ ${xl} | ${'true'} | ${false}
+ ${sm} | ${'true'} | ${false}
+ `(
+ 'toggle click sets page class to `page-with-super-sidebar-collapsed` when windowWidth is $windowWidth and cookie value is $cookie',
+ ({ windowWidth, cookie, hasClass }) => {
+ setHTMLFixture(`
+ <div class="page-with-super-sidebar ${cookie ? SIDEBAR_COLLAPSED_CLASS : ''}">
+ <aside class="super-sidebar"></aside>
+ <button class="js-super-sidebar-toggle"></button>
+ </div>
+ `);
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth);
+ getCookie.mockReturnValue(cookie);
+
+ bindSuperSidebarCollapsedEvents();
+
+ findToggles()[0].click();
+
+ pageHasCollapsedClass(hasClass);
+
+ if (windowWidth >= xl) {
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, !cookie, {
+ expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
+ });
+ } else {
+ expect(setCookie).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+});
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
new file mode 100644
index 00000000000..1f236616e77
--- /dev/null
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -0,0 +1,160 @@
+import {
+ getTopFrequentItems,
+ trackContextAccess,
+ formatContextSwitcherItems,
+} from '~/super_sidebar/utils';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import AccessorUtilities from '~/lib/utils/accessor';
+import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
+import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
+import { searchUserProjectsAndGroupsResponseMock } from './mock_data';
+
+describe('Super sidebar utils spec', () => {
+ describe('getTopFrequentItems', () => {
+ const maxItems = 3;
+
+ it('returns empty array if no items provided', () => {
+ const result = getTopFrequentItems();
+
+ expect(result.length).toBe(0);
+ });
+
+ it('returns the requested amount of items', () => {
+ const result = getTopFrequentItems(unsortedFrequentItems, maxItems);
+
+ expect(result.length).toBe(maxItems);
+ });
+
+ it('sorts frequent items in order of frequency and lastAccessedOn', () => {
+ const result = getTopFrequentItems(unsortedFrequentItems, maxItems);
+ const expectedResult = sortedFrequentItems.slice(0, maxItems);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('trackContextAccess', () => {
+ useLocalStorageSpy();
+
+ const username = 'root';
+ const context = {
+ namespace: 'groups',
+ item: { id: 1 },
+ };
+ const storageKey = `${username}/frequent-${context.namespace}`;
+
+ it('returns `false` if local storage is not available', () => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
+
+ expect(trackContextAccess()).toBe(false);
+ });
+
+ it('creates a new item if it does not exist in the local storage', () => {
+ trackContextAccess(username, context);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ storageKey,
+ JSON.stringify([
+ {
+ id: 1,
+ frequency: 1,
+ lastAccessedOn: Date.now(),
+ },
+ ]),
+ );
+ });
+
+ it('updates existing item if it was persisted to the local storage over 15 minutes ago', () => {
+ window.localStorage.setItem(
+ storageKey,
+ JSON.stringify([
+ {
+ id: 1,
+ frequency: 2,
+ lastAccessedOn: Date.now() - FIFTEEN_MINUTES_IN_MS - 1,
+ },
+ ]),
+ );
+ trackContextAccess(username, context);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ storageKey,
+ JSON.stringify([
+ {
+ id: 1,
+ frequency: 3,
+ lastAccessedOn: Date.now(),
+ },
+ ]),
+ );
+ });
+
+ it('leaves item as is if it was persisted to the local storage under 15 minutes ago', () => {
+ const jsonString = JSON.stringify([
+ {
+ id: 1,
+ frequency: 2,
+ lastAccessedOn: Date.now() - FIFTEEN_MINUTES_IN_MS,
+ },
+ ]);
+ window.localStorage.setItem(storageKey, jsonString);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(storageKey, jsonString);
+
+ trackContextAccess(username, context);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(3);
+ expect(window.localStorage.setItem).toHaveBeenLastCalledWith(storageKey, jsonString);
+ });
+
+ it('replaces the least popular item in the local storage once the persisted items limit has been hit', () => {
+ // Add the maximum amount of items to the local storage, in increasing popularity
+ const storedItems = Array.from({ length: FREQUENT_ITEMS.MAX_COUNT }).map((_, i) => ({
+ id: i + 1,
+ frequency: i + 1,
+ lastAccessedOn: Date.now(),
+ }));
+ // The first item is considered the least popular one as it has the lowest frequency (1)
+ const [leastPopularItem] = storedItems;
+ // Persist the list to the local storage
+ const jsonString = JSON.stringify(storedItems);
+ window.localStorage.setItem(storageKey, jsonString);
+ // Track some new item that hasn't been visited yet
+ const newItem = {
+ id: FREQUENT_ITEMS.MAX_COUNT + 1,
+ };
+ trackContextAccess(username, {
+ namespace: 'groups',
+ item: newItem,
+ });
+ // Finally, retrieve the final data from the local storage
+ const finallyStoredItems = JSON.parse(window.localStorage.getItem(storageKey));
+
+ expect(finallyStoredItems).not.toEqual(expect.arrayContaining([leastPopularItem]));
+ expect(finallyStoredItems).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: newItem.id,
+ frequency: 1,
+ }),
+ ]),
+ );
+ });
+ });
+
+ describe('formatContextSwitcherItems', () => {
+ it('returns the formatted items', () => {
+ const projects = searchUserProjectsAndGroupsResponseMock.data.projects.nodes;
+ expect(formatContextSwitcherItems(projects)).toEqual([
+ {
+ id: projects[0].id,
+ avatar: null,
+ title: projects[0].name,
+ subtitle: 'Gitlab Org',
+ link: projects[0].webUrl,
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/syntax_highlight_spec.js b/spec/frontend/syntax_highlight_spec.js
index 1be6c213350..a573c37ca44 100644
--- a/spec/frontend/syntax_highlight_spec.js
+++ b/spec/frontend/syntax_highlight_spec.js
@@ -1,14 +1,10 @@
-/* eslint-disable no-return-assign */
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import syntaxHighlight from '~/syntax_highlight';
describe('Syntax Highlighter', () => {
const stubUserColorScheme = (value) => {
- if (window.gon == null) {
- window.gon = {};
- }
- return (window.gon.user_color_scheme = value);
+ window.gon.user_color_scheme = value;
};
// We have to bind `document.querySelectorAll` to `document` to not mess up the fn's context
diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js
index b1726a2c0ef..8438bdb7db0 100644
--- a/spec/frontend/tags/components/delete_tag_modal_spec.js
+++ b/spec/frontend/tags/components/delete_tag_modal_spec.js
@@ -44,10 +44,6 @@ const findFormInput = () => wrapper.findComponent(GlFormInput);
const findForm = () => wrapper.find('form');
describe('Delete tag modal', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Deleting a regular tag', () => {
const expectedTitle = 'Delete tag. Are you ABSOLUTELY SURE?';
const expectedMessage = "You're about to permanently delete the tag test-tag.";
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index 99f61a31dbd..c60c6c79f17 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -37,10 +37,6 @@ describe('TermsApp', () => {
isLoggedIn.mockReturnValue(true);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findFormWithAction = (path) => wrapper.find(`form[action="${path}"]`);
const findButton = (path) => findFormWithAction(path).find('button[type="submit"]');
const findScrollableViewport = () => wrapper.findByTestId('scrollable-viewport');
diff --git a/spec/frontend/terraform/components/empty_state_spec.js b/spec/frontend/terraform/components/empty_state_spec.js
index 21bfff5f1be..db3de556244 100644
--- a/spec/frontend/terraform/components/empty_state_spec.js
+++ b/spec/frontend/terraform/components/empty_state_spec.js
@@ -16,10 +16,6 @@ describe('EmptyStateComponent', () => {
wrapper = shallowMount(EmptyState, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render content', () => {
expect(findEmptyState().props('title')).toBe(
"Your project doesn't have any Terraform state files",
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index 911bb8878da..ca9aa26a776 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -49,10 +49,6 @@ describe('InitCommandModal', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('on rendering', () => {
it('renders the explanatory text', () => {
expect(findExplanatoryText().text()).toContain('personal access token');
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 40b7448d78d..31a644b39b4 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlModal, GlSprintf, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -85,6 +85,7 @@ describe('StatesTableActions', () => {
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
const findRemoveModal = () => wrapper.findComponent(GlModal);
+ const findFormInput = () => wrapper.findComponent(GlFormInput);
beforeEach(() => {
return createComponent();
@@ -96,7 +97,6 @@ describe('StatesTableActions', () => {
toast = null;
unlockResponse = null;
updateStateResponse = null;
- wrapper.destroy();
});
describe('when the state is loading', () => {
@@ -296,9 +296,7 @@ describe('StatesTableActions', () => {
describe('when state name is present', () => {
beforeEach(async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ removeConfirmText: defaultProps.state.name });
+ await findFormInput().vm.$emit('input', defaultProps.state.name);
findRemoveModal().vm.$emit('ok');
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index 0b3b169891b..7c783c9f717 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -127,7 +127,7 @@ describe('StatesTable', () => {
propsData,
provide: { projectPath: 'path/to/project' },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
}),
);
@@ -140,11 +140,6 @@ describe('StatesTable', () => {
return createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
name | toolTipText | hasBadge | loading | lineNumber
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index 580951e799a..dc59e2769f6 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -63,11 +63,6 @@ describe('TerraformList', () => {
const findStatesTable = () => wrapper.findComponent(StatesTable);
const findTab = () => wrapper.findComponent(GlTab);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when the terraform query has succeeded', () => {
describe('when there is a list of terraform states', () => {
const states = [
diff --git a/spec/frontend/toggles/index_spec.js b/spec/frontend/toggles/index_spec.js
index f8c43e0ad0c..89e35991914 100644
--- a/spec/frontend/toggles/index_spec.js
+++ b/spec/frontend/toggles/index_spec.js
@@ -44,7 +44,6 @@ describe('toggles/index.js', () => {
afterEach(() => {
document.body.innerHTML = '';
instance = null;
- toggleWrapper = null;
});
describe('initToggle', () => {
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
index fcd1a33fa68..1ca58053e68 100644
--- a/spec/frontend/token_access/inbound_token_access_spec.js
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
@@ -26,7 +26,7 @@ const error = new Error(message);
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('TokenAccess component', () => {
let wrapper;
diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js
index 3a68f247aa6..cdb385aa743 100644
--- a/spec/frontend/token_access/opt_in_jwt_spec.js
+++ b/spec/frontend/token_access/opt_in_jwt_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { OPT_IN_JWT_HELP_LINK } from '~/token_access/constants';
import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql';
@@ -16,7 +16,7 @@ const error = new Error(errorMessage);
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('OptInJwt component', () => {
let wrapper;
diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 893a021197f..347ea1178bc 100644
--- a/spec/frontend/token_access/outbound_token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import OutboundTokenAccess from '~/token_access/components/outbound_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';
@@ -26,7 +26,7 @@ const error = new Error(message);
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('TokenAccess component', () => {
let wrapper;
diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js
index 7f269ee5fda..cff16fd125c 100644
--- a/spec/frontend/token_access/token_access_app_spec.js
+++ b/spec/frontend/token_access/token_access_app_spec.js
@@ -11,12 +11,8 @@ describe('TokenAccessApp component', () => {
const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess);
const findOptInJwt = () => wrapper.findComponent(OptInJwt);
- const createComponent = (flagState = false) => {
- wrapper = shallowMount(TokenAccessApp, {
- provide: {
- glFeatures: { ciInboundJobTokenScope: flagState },
- },
- });
+ const createComponent = () => {
+ wrapper = shallowMount(TokenAccessApp);
};
describe('default', () => {
@@ -32,12 +28,6 @@ describe('TokenAccessApp component', () => {
expect(findOutboundTokenAccess().exists()).toBe(true);
});
- it('does not render the inbound token access component', () => {
- expect(findInboundTokenAccess().exists()).toBe(false);
- });
- });
-
- describe('with feature flag enabled', () => {
it('renders the inbound token access component', () => {
createComponent(true);
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index b51d8b3ccea..7654aa09b0a 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -6,14 +6,19 @@ import { mockProjects, mockFields } from './mock_data';
describe('Token projects table', () => {
let wrapper;
- const createComponent = () => {
+ const defaultProps = {
+ tableFields: mockFields,
+ projects: mockProjects,
+ };
+
+ const createComponent = (props) => {
wrapper = mountExtended(TokenProjectsTable, {
provide: {
fullPath: 'root/ci-project',
},
propsData: {
- tableFields: mockFields,
- projects: mockProjects,
+ ...defaultProps,
+ ...props,
},
});
};
@@ -25,31 +30,52 @@ describe('Token projects table', () => {
const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace');
- beforeEach(() => {
+ it('displays a table', () => {
createComponent();
- });
- it('displays a table', () => {
expect(findTable().exists()).toBe(true);
});
it('displays the correct amount of table rows', () => {
+ createComponent();
+
expect(findAllTableRows()).toHaveLength(mockProjects.length);
});
it('delete project button emits event with correct project to delete', async () => {
+ createComponent();
+
await findDeleteProjectBtn().trigger('click');
expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]);
});
it('does not show the remove icon if the project is locked', () => {
+ createComponent();
+
// currently two mock projects with one being a locked project
expect(findAllDeleteProjectBtn()).toHaveLength(1);
});
it('displays project and namespace cells', () => {
+ createComponent();
+
expect(findProjectNameCell().text()).toBe('merge-train-stuff');
expect(findProjectNamespaceCell().text()).toBe('root');
});
+
+ it('displays empty string for namespace when namespace is null', () => {
+ const nullNamespace = {
+ id: '1',
+ name: 'merge-train-stuff',
+ namespace: null,
+ fullPath: 'root/merge-train-stuff',
+ isLocked: false,
+ __typename: 'Project',
+ };
+
+ createComponent({ projects: [nullNamespace] });
+
+ expect(findProjectNamespaceCell().text()).toBe('');
+ });
});
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
index d5a63a99601..e473091f405 100644
--- a/spec/frontend/tooltips/components/tooltips_spec.js
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -30,11 +30,6 @@ describe('tooltips/components/tooltips.vue', () => {
const allTooltips = () => wrapper.findAllComponents(GlTooltip);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('addTooltips', () => {
let target;
diff --git a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
index cb70ea4e72d..3508bf7cfde 100644
--- a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
+++ b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
@@ -23,10 +23,6 @@ describe('UsageQuotasApp', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle');
it('renders the view title', () => {
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
index 3379af3f41c..1a200090805 100644
--- a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
@@ -53,10 +53,6 @@ describe('ProjectStorageApp', () => {
const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link');
const findUsageGraph = () => wrapper.findComponent(UsageGraph);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with apollo fetching successful', () => {
let mockApollo;
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
index ce489f69cad..6065ec9e4bf 100644
--- a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
@@ -54,9 +54,6 @@ describe('ProjectStorageDetail', () => {
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
describe('with storage types', () => {
it.each(storageTypes)(
diff --git a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
index 1eb3386bfb8..364cf1e587b 100644
--- a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
@@ -16,10 +16,6 @@ describe('StorageTypeIcon', () => {
const findGlIcon = () => wrapper.findComponent(GlIcon);
describe('rendering icon', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
expected | provided
${'doc-image'} | ${'lfsObjectsSize'}
diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
index 75b970d937a..02268e1c9d8 100644
--- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
@@ -39,10 +39,6 @@ describe('Storage Counter usage graph component', () => {
mountComponent(data);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the legend in order', () => {
const types = wrapper.findAll('[data-testid="storage-type-legend"]');
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js
index 161eb036361..603289ac11e 100644
--- a/spec/frontend/user_lists/components/user_lists_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_spec.js
@@ -39,11 +39,6 @@ describe('~/user_lists/components/user_lists.vue', () => {
const newButton = () => within(wrapper.element).queryAllByText('New user list');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('without permissions', () => {
const provideData = {
...mockProvide,
diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js
index 3324b040b86..96e9705f02b 100644
--- a/spec/frontend/user_lists/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -22,10 +22,6 @@ describe('User Lists Table', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should display the details of a user list', () => {
expect(wrapper.find('[data-testid="ffUserListName"]').text()).toBe(userList.name);
expect(wrapper.find('[data-testid="ffUserListIds"]').text()).toBe(
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 8ce071c075f..3808cc8b0fc 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -10,8 +10,6 @@ jest.mock('~/api/user_api', () => ({
}));
describe('User Popovers', () => {
- let origGon;
-
const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html';
const selector = '.js-user-link[data-user], .js-user-link[data-user-id]';
@@ -60,15 +58,6 @@ describe('User Popovers', () => {
});
};
- beforeEach(() => {
- origGon = window.gon;
- window.gon = {};
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
-
describe('when signed out', () => {
beforeEach(() => {
setupTestSubject();
diff --git a/spec/frontend/validators/length_validator_spec.js b/spec/frontend/validators/length_validator_spec.js
new file mode 100644
index 00000000000..ece8238b3e3
--- /dev/null
+++ b/spec/frontend/validators/length_validator_spec.js
@@ -0,0 +1,91 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import LengthValidator, { isAboveMaxLength, isBelowMinLength } from '~/validators/length_validator';
+
+describe('length_validator', () => {
+ describe('isAboveMaxLength', () => {
+ it('should return true if the string is longer than the maximum length', () => {
+ expect(isAboveMaxLength('123456', '5')).toBe(true);
+ });
+
+ it('should return false if the string is shorter than the maximum length', () => {
+ expect(isAboveMaxLength('1234', '5')).toBe(false);
+ });
+ });
+
+ describe('isBelowMinLength', () => {
+ it('should return true if the string is shorter than the minimum length and not empty', () => {
+ expect(isBelowMinLength('1234', '5', 'false')).toBe(true);
+ });
+
+ it('should return false if the string is longer than the minimum length', () => {
+ expect(isBelowMinLength('123456', '5', 'false')).toBe(false);
+ });
+
+ it('should return false if the string is empty and allowed to be empty', () => {
+ expect(isBelowMinLength('', '5', 'true')).toBe(false);
+ });
+
+ it('should return true if the string is empty and not allowed to be empty', () => {
+ expect(isBelowMinLength('', '5', 'false')).toBe(true);
+ });
+ });
+
+ describe('LengthValidator', () => {
+ let input;
+ let validator;
+
+ beforeEach(() => {
+ setHTMLFixture(
+ '<div class="container"><input class="js-validate-length" /><span class="gl-field-error"></span></div>',
+ );
+ input = document.querySelector('input');
+ input.dataset.minLength = '3';
+ input.dataset.maxLength = '5';
+ input.dataset.minLengthMessage = 'Too short';
+ input.dataset.maxLengthMessage = 'Too long';
+ validator = new LengthValidator({ container: '.container' });
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('sets error message for input with value longer than max length', () => {
+ input.value = '123456';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too long');
+ });
+
+ it('sets error message for input with value shorter than min length', () => {
+ input.value = '12';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too short');
+ });
+
+ it('does not set error message for input with valid length', () => {
+ input.value = '123';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBeNull();
+ });
+
+ it('does not set error message for empty input if allowEmpty is true', () => {
+ input.dataset.allowEmpty = 'true';
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBeNull();
+ });
+
+ it('sets error message for empty input if allowEmpty is false', () => {
+ input.dataset.allowEmpty = 'false';
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too short');
+ });
+
+ it('sets error message for empty input if allowEmpty is not defined', () => {
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too short');
+ });
+ });
+});
diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js
new file mode 100644
index 00000000000..b6234c51535
--- /dev/null
+++ b/spec/frontend/vue_compat_test_setup.js
@@ -0,0 +1,60 @@
+/* eslint-disable import/no-commonjs */
+const Vue = require('vue');
+const VTU = require('@vue/test-utils');
+const { installCompat: installVTUCompat, fullCompatConfig } = require('vue-test-utils-compat');
+
+if (global.document) {
+ const compatConfig = {
+ MODE: 2,
+
+ GLOBAL_MOUNT: 'suppress-warning',
+ GLOBAL_EXTEND: 'suppress-warning',
+ GLOBAL_PROTOTYPE: 'suppress-warning',
+ RENDER_FUNCTION: 'suppress-warning',
+
+ INSTANCE_DESTROY: 'suppress-warning',
+ INSTANCE_DELETE: 'suppress-warning',
+
+ INSTANCE_ATTRS_CLASS_STYLE: 'suppress-warning',
+ INSTANCE_CHILDREN: 'suppress-warning',
+ INSTANCE_SCOPED_SLOTS: 'suppress-warning',
+ INSTANCE_LISTENERS: 'suppress-warning',
+ INSTANCE_EVENT_EMITTER: 'suppress-warning',
+ INSTANCE_EVENT_HOOKS: 'suppress-warning',
+ INSTANCE_SET: 'suppress-warning',
+ GLOBAL_OBSERVABLE: 'suppress-warning',
+ GLOBAL_SET: 'suppress-warning',
+ COMPONENT_FUNCTIONAL: 'suppress-warning',
+ COMPONENT_V_MODEL: 'suppress-warning',
+ CUSTOM_DIR: 'suppress-warning',
+ OPTIONS_BEFORE_DESTROY: 'suppress-warning',
+ OPTIONS_DATA_MERGE: 'suppress-warning',
+ OPTIONS_DATA_FN: 'suppress-warning',
+ OPTIONS_DESTROYED: 'suppress-warning',
+ ATTR_FALSE_VALUE: 'suppress-warning',
+
+ COMPILER_V_ON_NATIVE: 'suppress-warning',
+ COMPILER_V_BIND_OBJECT_ORDER: 'suppress-warning',
+
+ CONFIG_WHITESPACE: 'suppress-warning',
+ CONFIG_OPTION_MERGE_STRATS: 'suppress-warning',
+ PRIVATE_APIS: 'suppress-warning',
+ WATCH_ARRAY: 'suppress-warning',
+ };
+
+ let compatH;
+ Vue.config.compilerOptions.whitespace = 'condense';
+ Vue.createApp({
+ compatConfig: {
+ MODE: 3,
+ RENDER_FUNCTION: 'suppress-warning',
+ },
+ render(h) {
+ compatH = h;
+ },
+ }).mount(document.createElement('div'));
+
+ Vue.configureCompat(compatConfig);
+ installVTUCompat(VTU, fullCompatConfig, compatH);
+ VTU.config.global.renderStubDefaultSlot = true;
+}
diff --git a/spec/frontend/vue_merge_request_widget/components/action_buttons.js b/spec/frontend/vue_merge_request_widget/components/action_buttons.js
index 6d714aeaf18..7334f061dc9 100644
--- a/spec/frontend/vue_merge_request_widget/components/action_buttons.js
+++ b/spec/frontend/vue_merge_request_widget/components/action_buttons.js
@@ -11,10 +11,6 @@ function factory(propsData = {}) {
}
describe('MR widget extension actions', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('tertiaryButtons', () => {
it('renders buttons', () => {
factory({
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 063425454d7..4164a7df482 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
@@ -14,10 +14,6 @@ function factory(propsData) {
}
describe('Widget added commit message', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays changes where not merged when state is closed', () => {
factory({ state: 'closed' });
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index bf208f16d18..e78e1be7882 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -1,26 +1,32 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createAlert } from '~/flash';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createAlert } from '~/alert';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
import {
- FETCH_LOADING,
- FETCH_ERROR,
APPROVE_ERROR,
UNAPPROVE_ERROR,
} from '~/vue_merge_request_widget/components/approvals/messages';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
+import { createCanApproveResponse } from 'jest/approvals/mock_data';
+
+Vue.use(VueApollo);
const mockAlertDismiss = jest.fn();
-jest.mock('~/flash', () => ({
+jest.mock('~/alert', () => ({
createAlert: jest.fn().mockImplementation(() => ({
dismiss: mockAlertDismiss,
})),
}));
-const RULE_NAME = 'first_rule';
const TEST_HELP_PATH = 'help/path';
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
const testApprovals = () => ({
@@ -34,15 +40,18 @@ const testApprovals = () => ({
require_password_to_approve: false,
invalid_approvers_rules: [],
});
-const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] });
describe('MRWidget approvals', () => {
let wrapper;
let service;
let mr;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, response = approvedByCurrentUser) => {
+ const requestHandlers = [[approvedByQuery, jest.fn().mockResolvedValue(response)]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMount(Approvals, {
+ apolloProvider,
propsData: {
mr,
service,
@@ -68,15 +77,10 @@ describe('MRWidget approvals', () => {
};
const findSummary = () => wrapper.findComponent(ApprovalsSummary);
const findOptionalSummary = () => wrapper.findComponent(ApprovalsSummaryOptional);
- const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]');
beforeEach(() => {
service = {
...{
- fetchApprovals: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
- fetchApprovalSettings: jest
- .fn()
- .mockReturnValue(Promise.resolve(testApprovalRulesResponse())),
approveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
unapproveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
approveMergeRequestWithAuth: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
@@ -97,55 +101,21 @@ describe('MRWidget approvals', () => {
};
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- });
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when created', () => {
- it('shows loading message', async () => {
- service = {
- fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})),
- };
-
- createComponent();
- await nextTick();
- expect(wrapper.text()).toContain(FETCH_LOADING);
- });
-
- it('fetches approvals', () => {
- createComponent();
- expect(service.fetchApprovals).toHaveBeenCalled();
- });
- });
-
- describe('when fetch approvals error', () => {
- beforeEach(() => {
- jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject());
- createComponent();
- return nextTick();
- });
-
- it('still shows loading message', () => {
- expect(wrapper.text()).toContain(FETCH_LOADING);
- });
-
- it('flashes error', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR });
- });
+ gon.current_user_id = getIdFromGraphQLId(
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes[0].id,
+ );
});
describe('action button', () => {
describe('when mr is closed', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ const response = createCanApproveResponse();
+
mr.isOpen = false;
- mr.approvals.user_has_approved = false;
- mr.approvals.user_can_approve = true;
- createComponent();
- return nextTick();
+ createComponent({}, response);
+ await waitForPromises();
});
it('action is not rendered', () => {
@@ -154,12 +124,12 @@ describe('MRWidget approvals', () => {
});
describe('when user cannot approve', () => {
- beforeEach(() => {
- mr.approvals.user_has_approved = false;
- mr.approvals.user_can_approve = false;
+ beforeEach(async () => {
+ const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
+ response.data.project.mergeRequest.approvedBy.nodes = [];
- createComponent();
- return nextTick();
+ createComponent({}, response);
+ await waitForPromises();
});
it('action is not rendered', () => {
@@ -168,15 +138,16 @@ describe('MRWidget approvals', () => {
});
describe('when user can approve', () => {
+ let canApproveResponse;
+
beforeEach(() => {
- mr.approvals.user_has_approved = false;
- mr.approvals.user_can_approve = true;
+ canApproveResponse = createCanApproveResponse();
});
describe('and MR is unapproved', () => {
- beforeEach(() => {
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ createComponent({}, canApproveResponse);
+ await waitForPromises();
});
it('approve action is rendered', () => {
@@ -190,30 +161,33 @@ describe('MRWidget approvals', () => {
describe('and MR is approved', () => {
beforeEach(() => {
- mr.approvals.approved = true;
+ canApproveResponse.data.project.mergeRequest.approved = true;
});
describe('with no approvers', () => {
- beforeEach(() => {
- mr.approvals.approved_by = [];
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes = [];
+ createComponent({}, canApproveResponse);
+ await nextTick();
});
- it('approve action (with inverted style) is rendered', () => {
- expect(findActionData()).toEqual({
+ it('approve action is rendered', () => {
+ expect(findActionData()).toMatchObject({
variant: 'confirm',
text: 'Approve',
- category: 'secondary',
});
});
});
describe('with approvers', () => {
- beforeEach(() => {
- mr.approvals.approved_by = [{ user: { id: 7 } }];
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes =
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes;
+
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 2;
+
+ createComponent({}, canApproveResponse);
+ await waitForPromises();
});
it('approve additionally action is rendered', () => {
@@ -227,9 +201,9 @@ describe('MRWidget approvals', () => {
});
describe('when approve action is clicked', () => {
- beforeEach(() => {
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ createComponent({}, canApproveResponse);
+ await waitForPromises();
});
it('shows loading icon', () => {
@@ -258,10 +232,6 @@ describe('MRWidget approvals', () => {
it('emits to eventHub', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
});
-
- it('calls store setApprovals', () => {
- expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals());
- });
});
describe('and error', () => {
@@ -286,12 +256,12 @@ describe('MRWidget approvals', () => {
});
describe('when user has approved', () => {
- beforeEach(() => {
- mr.approvals.user_has_approved = true;
- mr.approvals.user_can_approve = false;
+ beforeEach(async () => {
+ const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
- createComponent();
- return nextTick();
+ createComponent({}, response);
+
+ await waitForPromises();
});
it('revoke action is rendered', () => {
@@ -316,10 +286,6 @@ describe('MRWidget approvals', () => {
it('emits to eventHub', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
});
-
- it('calls store setApprovals', () => {
- expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals());
- });
});
describe('and error', () => {
@@ -329,7 +295,7 @@ describe('MRWidget approvals', () => {
return nextTick();
});
- it('flashes error message', () => {
+ it('alerts error message', () => {
expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
});
});
@@ -338,19 +304,24 @@ describe('MRWidget approvals', () => {
});
describe('approvals optional summary', () => {
+ let optionalApprovalsResponse;
+
+ beforeEach(() => {
+ optionalApprovalsResponse = JSON.parse(JSON.stringify(approvedByCurrentUser));
+ });
+
describe('when no approvals required and no approvers', () => {
beforeEach(() => {
- mr.approvals.approved_by = [];
- mr.approvals.approvals_required = 0;
- mr.approvals.user_has_approved = false;
+ optionalApprovalsResponse.data.project.mergeRequest.approvedBy.nodes = [];
+ optionalApprovalsResponse.data.project.mergeRequest.approvalsRequired = 0;
});
describe('and can approve', () => {
- beforeEach(() => {
- mr.approvals.user_can_approve = true;
+ beforeEach(async () => {
+ optionalApprovalsResponse.data.project.mergeRequest.userPermissions.canApprove = true;
- createComponent();
- return nextTick();
+ createComponent({}, optionalApprovalsResponse);
+ await waitForPromises();
});
it('is shown', () => {
@@ -363,11 +334,9 @@ describe('MRWidget approvals', () => {
});
describe('and cannot approve', () => {
- beforeEach(() => {
- mr.approvals.user_can_approve = false;
-
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ createComponent({}, optionalApprovalsResponse);
+ await nextTick();
});
it('is shown', () => {
@@ -382,9 +351,9 @@ describe('MRWidget approvals', () => {
});
describe('approvals summary', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- return nextTick();
+ await nextTick();
});
it('is rendered with props', () => {
@@ -393,41 +362,7 @@ describe('MRWidget approvals', () => {
expect(findOptionalSummary().exists()).toBe(false);
expect(summary.exists()).toBe(true);
expect(summary.props()).toMatchObject({
- projectPath: 'gitlab-org/gitlab',
- iid: '1',
- updatedCount: 0,
- });
- });
- });
-
- describe('invalid rules', () => {
- beforeEach(() => {
- mr.approvals.merge_request_approvers_available = true;
- createComponent();
- });
-
- it('does not render related components', () => {
- expect(findInvalidRules().exists()).toBe(false);
- });
-
- describe('when invalid rules are present', () => {
- beforeEach(() => {
- mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }];
- createComponent();
- });
-
- it('renders related components', () => {
- const invalidRules = findInvalidRules();
-
- expect(invalidRules.exists()).toBe(true);
-
- const invalidRulesText = invalidRules.text();
-
- expect(invalidRulesText).toContain(RULE_NAME);
- expect(invalidRulesText).toContain(
- 'GitLab has approved this rule automatically to unblock the merge request.',
- );
- expect(invalidRulesText).toContain('Learn more.');
+ approvalState: approvedByCurrentUser.data.project.mergeRequest,
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
index e6fb0495947..bf3df70d423 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
@@ -13,11 +13,6 @@ describe('MRWidget approvals summary optional', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findHelpLink = () => wrapper.findComponent(GlLink);
describe('when can approve', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
index e75ce7c60c9..8c6b3cc464c 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
@@ -1,11 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mount } from '@vue/test-utils';
-import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_multiple_users.json';
-import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_no_approvals.json';
-import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql.json';
+import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_multiple_users.json';
+import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_no_approvals.json';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import {
@@ -14,32 +13,22 @@ import {
APPROVED_BY_YOU_AND_OTHERS,
} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
-import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approved_by.query.graphql';
Vue.use(VueApollo);
describe('MRWidget approvals summary', () => {
- const originalUserId = gon.current_user_id;
let wrapper;
- const createComponent = (response = approvedByCurrentUser) => {
+ const createComponent = (data = approvedByCurrentUser) => {
wrapper = mount(ApprovalsSummary, {
propsData: {
- projectPath: 'gitlab-org/gitlab',
- iid: '1',
+ approvalState: data.data.project.mergeRequest,
},
- apolloProvider: createMockApollo([[approvedByQuery, jest.fn().mockResolvedValue(response)]]),
});
};
const findAvatars = () => wrapper.findComponent(UserAvatarList);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- gon.current_user_id = originalUserId;
- });
-
describe('when approved', () => {
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
index 52e2393bf05..332f14a1721 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
@@ -26,7 +26,6 @@ describe('Merge Requests Artifacts list app', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
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 b7bf72cd215..bb049a5d52f 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
@@ -18,10 +18,6 @@ describe('Artifacts List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
mountComponent(data);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
index 198a4c2823a..3a621db7b44 100644
--- a/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
@@ -20,11 +20,6 @@ function factory(propsData) {
}
describe('MR widget extension child content', () => {
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders child components', () => {
factory({
data: {
diff --git a/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
index f3aa5bb774f..ffa6b5538d3 100644
--- a/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
@@ -11,10 +11,6 @@ function factory(propsData = {}) {
}
describe('MR widget extensions status icon', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders loading icon', () => {
factory({ name: 'test', isLoading: true, iconName: 'failed' });
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
index 81f266d8070..6b22c2e26ac 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
@@ -25,10 +25,6 @@ describe('Merge Request Collapsible Extension', () => {
const findErrorMessage = () => wrapper.find('.js-error-state');
const findIcon = () => wrapper.findComponent(GlIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while collapsed', () => {
beforeEach(() => {
mountComponent(data);
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
index 5d923d0383f..01178dab9bb 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
@@ -11,10 +11,6 @@ function createComponent(propsData = {}) {
}
describe('MrWidgetAlertMessage', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a GlAert', () => {
createComponent({ type: 'danger' });
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
index 8a42e2e2ce7..7eafccae083 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
@@ -29,7 +29,6 @@ describe('MrWidgetAuthor', () => {
});
afterEach(() => {
- wrapper.destroy();
window.gl = oldWindowGl;
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
index 90a29d15488..534b745aed2 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
@@ -23,10 +23,6 @@ describe('MrWidgetAuthorTime', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders provided action text', () => {
expect(wrapper.text()).toContain('Merged by');
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
index 8dadb0c65d0..25de76ba33c 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
@@ -13,10 +13,6 @@ describe('MrWidgetContainer', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has layout', () => {
factory();
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
index 6a9b019fb4f..090a96d576c 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
@@ -15,10 +15,6 @@ describe('MrWidgetIcon', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders icon and container', () => {
expect(wrapper.element.className).toContain('circle-icon-container');
expect(wrapper.findComponent(GlIcon).props('name')).toEqual(TEST_ICON);
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
index 13beb43e10b..18842e996de 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
@@ -29,10 +29,6 @@ describe('MrWidgetPipelineContainer', () => {
mock.onGet().reply(HTTP_STATUS_OK, {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDeploymentList = () => wrapper.findComponent(DeploymentList);
const findCIErrorMessage = () => wrapper.findByTestId('ci-error-message');
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 ec047fe0714..f284ec98a73 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
@@ -1,3 +1,4 @@
+import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
@@ -8,8 +9,11 @@ jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
-function createWrapper(propsData) {
+function createWrapper(propsData, provideData) {
wrapper = mount(WidgetRebase, {
+ provide: {
+ ...provideData,
+ },
propsData,
data() {
return {
@@ -19,6 +23,7 @@ function createWrapper(propsData) {
userPermissions: {
pushToSourceBranch: propsData.mr.canPushToSourceBranch,
},
+ pipelines: propsData.mr.pipelines,
},
};
},
@@ -37,11 +42,8 @@ describe('Merge request widget rebase component', () => {
const findRebaseMessageText = () => findRebaseMessage().text();
const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]');
const findRebaseWithoutCiButton = () => wrapper.find('[data-testid="rebase-without-ci-button"]');
+ const findModal = () => wrapper.findComponent(GlModal);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
describe('while rebasing', () => {
it('should show progress message', () => {
createWrapper({
@@ -199,6 +201,72 @@ describe('Merge request widget rebase component', () => {
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
+
+ describe('security modal', () => {
+ it('displays modal and rebases after confirming', () => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'user/forked',
+ },
+ },
+ ],
+ },
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ { canCreatePipelineInTargetProject: true },
+ );
+
+ findModal().vm.show = jest.fn();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(findModal().vm.show).toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+
+ it('does not display modal', () => {
+ createWrapper(
+ {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ { canCreatePipelineInTargetProject: false },
+ );
+
+ findModal().vm.show = jest.fn();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(findModal().vm.show).not.toHaveBeenCalled();
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+ });
});
describe('without permissions', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
index 15522f7ac1d..42a16090510 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
@@ -9,10 +9,6 @@ describe('MRWidgetRelatedLinks', () => {
wrapper = shallowMount(RelatedLinks, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('closesText', () => {
it('returns Closes text for open merge request', () => {
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 530549b7b9c..b210327aa31 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
@@ -17,11 +17,6 @@ describe('MR widget status icon component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('while loading', () => {
it('renders loading icon', () => {
createWrapper({ status: 'loading' });
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 73358edee78..70c76687a79 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
@@ -18,10 +18,6 @@ describe('MRWidgetSuggestPipeline', () => {
describe('template', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('core functionality', () => {
const findOkBtn = () => wrapper.find('[data-testid="ok"]');
let trackingSpy;
diff --git a/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
index e393b56034d..48484551d59 100644
--- a/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
@@ -17,10 +17,6 @@ describe('review app link', () => {
wrapper = shallowMount(ReviewAppLink, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders provided link as href attribute', () => {
expect(wrapper.attributes('href')).toBe(props.link);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
index c0add94e6ed..f520c6a4f78 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
@@ -26,10 +26,6 @@ describe('Commits edit component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTextarea = () => wrapper.find('.form-control');
it('has a correct label', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index e4448346685..c2ab0e384e8 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -12,10 +12,6 @@ function factory(propsData = {}) {
}
describe('Merge request widget merge checks failed state component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
mrState | displayText
${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
index c9aca01083d..7d471b91c37 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
@@ -37,10 +37,6 @@ describe('MergeFailedPipelineConfirmationDialog', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render informational text explaining why merging immediately can be dangerous', () => {
expect(trimText(wrapper.text())).toContain(
'The latest pipeline for this merge request did not succeed. The latest changes are unverified. Are you sure you want to attempt to merge?',
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 08700e834d7..3e18ee75125 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
@@ -10,11 +10,6 @@ describe('MRWidgetArchived', () => {
wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders error icon', () => {
expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
expect(wrapper.findComponent(StateContainer).props().status).toBe('failed');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index fef5fee5f19..65d170cae8b 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -83,8 +83,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
afterEach(() => {
window.gl = oldWindowGl;
- wrapper.destroy();
- wrapper = null;
});
describe('computed', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 826f708069c..9b043bda72d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -18,10 +18,6 @@ describe('MRWidgetAutoMergeFailed', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
createComponent({
mr: { mergeError },
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 ac18ccf9e26..6c3b7f76fe6 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
@@ -9,11 +9,6 @@ describe('MRWidgetChecking', () => {
wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders loading icon', () => {
expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
expect(wrapper.findComponent(StateContainer).props().status).toBe('loading');
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 5d2d1fdd6f1..e4febda1daa 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
@@ -36,10 +36,6 @@ describe('Commits message dropdown component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
index a6d3a6286a7..b3843b066df 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
@@ -21,10 +21,6 @@ describe('Commits header component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count');
const findCommitToggle = () => wrapper.find('.commit-edit-toggle');
const findTargetBranchMessage = () => wrapper.find('.label-branch');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index 2ca9dc61745..7f0a171d712 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -50,10 +50,6 @@ describe('MRWidgetConflicts', () => {
await nextTick();
}
- afterEach(() => {
- wrapper.destroy();
- });
-
// There are two permissions we need to consider:
//
// 1. Is the user allowed to merge to the target branch?
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 833fa27d453..38e5422325a 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
@@ -28,10 +28,6 @@ describe('MRWidgetFailedToMerge', () => {
jest.spyOn(window, 'clearInterval').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('interval', () => {
it('sets interval to refresh', () => {
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
index a3aa563b516..e44e2834a0e 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
@@ -62,10 +62,6 @@ describe('MRWidgetMerged', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButtonByText = (text) =>
wrapper.findAll('button').wrappers.find((w) => w.text() === text);
const findRemoveSourceBranchButton = () => findButtonByText('Delete source branch');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
index 5408f731b34..ca75ca11e5b 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
@@ -29,10 +29,6 @@ describe('MRWidgetMerging', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders information about merge request being merged', () => {
const message = wrapper.findComponent(BoldText).props('message');
expect(message).toContain('Merging!');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
index f29cf55f7ce..fca25b8bb94 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
@@ -15,10 +15,6 @@ function factory(sourceBranchRemoved) {
}
describe('MRWidgetMissingBranch', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
sourceBranchRemoved | branchName
${true} | ${'source'}
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 42515c597c5..40b053282de 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
@@ -10,11 +10,6 @@ describe('MRWidgetNotAllowed', () => {
wrapper = shallowMount(notAllowedComponent);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders success icon', () => {
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('success');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
index 6de0c06c33d..c8fa1399dcb 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -1,28 +1,58 @@
-import Vue, { nextTick } from 'vue';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NothingToMerge from '~/vue_merge_request_widget/components/states/nothing_to_merge.vue';
describe('NothingToMerge', () => {
- describe('template', () => {
- const Component = Vue.extend(NothingToMerge);
- const newBlobPath = '/foo';
- const vm = new Component({
- el: document.createElement('div'),
+ let wrapper;
+ const newBlobPath = '/foo';
+
+ const defaultProps = {
+ mr: {
+ newBlobPath,
+ },
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(NothingToMerge, {
propsData: {
- mr: { newBlobPath },
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
},
});
+ };
+
+ const findCreateButton = () => wrapper.findByTestId('createFileButton');
+ const findNothingToMergeTextBody = () => wrapper.findByTestId('nothing-to-merge-body');
+
+ describe('With Blob link', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the component with the correct text and highlights', () => {
+ expect(wrapper.text()).toContain('This merge request contains no changes.');
+ expect(findNothingToMergeTextBody().text()).toContain(
+ 'Use merge requests to propose changes to your project and discuss them with your team. To make changes, push a commit or edit this merge request to use a different branch.',
+ );
+ });
+
+ it('shows the Create file button with the correct attributes', () => {
+ const createButton = findCreateButton();
+
+ expect(createButton.exists()).toBe(true);
+ expect(createButton.attributes('href')).toBe(newBlobPath);
+ });
+ });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBe(true);
- expect(vm.$el.querySelector('[data-testid="createFileButton"]').href).toContain(newBlobPath);
- expect(vm.$el.innerText).toContain('Use merge requests to propose changes to your project');
+ describe('Without Blob link', () => {
+ beforeEach(() => {
+ createComponent({ mr: { newBlobPath: '' } });
});
- it('should not show new blob link if there is no link available', () => {
- vm.mr.newBlobPath = null;
- nextTick(() => {
- expect(vm.$el.querySelector('[data-testid="createFileButton"]')).toEqual(null);
- });
+ it('does not show the Create file button', () => {
+ expect(findCreateButton().exists()).toBe(false);
});
});
});
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 c0197b5e20a..d99106df0a2 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
@@ -10,11 +10,6 @@ describe('MRWidgetPipelineBlocked', () => {
wrapper = shallowMount(PipelineBlockedComponent);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders error icon', () => {
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
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 8bae2b62ed1..ea93463f3ab 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
@@ -19,11 +19,6 @@ describe('PipelineFailed', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render error status icon', () => {
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
index aaa4591d67d..02b71ebf183 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -20,10 +20,6 @@ describe('ShaMismatch', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render warning message', () => {
expect(wrapper.text()).toContain('Merge blocked: new changes were just added.');
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
index c839fa17fe5..97f8e695df9 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -14,10 +14,6 @@ describe('Squash before merge component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
describe('checkbox', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
index c97b42f61ac..58b9f162815 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -21,10 +21,6 @@ describe('UnresolvedDiscussions', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('triggers the correct notes event when the jump to first unresolved discussion button is clicked', () => {
jest.spyOn(notesEventHub, '$emit');
@@ -38,10 +34,6 @@ describe('UnresolvedDiscussions', () => {
wrapper = createComponent({ path: TEST_HOST });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should have correct elements', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain('Merge blocked:');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
index 5ec9654a4af..20d06a7aaee 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
@@ -15,10 +15,6 @@ function factory({ canMerge }) {
}
describe('New ready to merge state component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
canMerge
${true}
diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
index e610ceb2122..43ce1769ff3 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import WorkInProgress, {
MSG_SOMETHING_WENT_WRONG,
MSG_MARK_READY,
@@ -22,7 +22,7 @@ const TEST_MR_IID = '23';
const TEST_MR_TITLE = 'Test MR Title';
const TEST_PROJECT_PATH = 'lorem/ipsum';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/merge_request');
describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
index 366ea113162..adefce9060c 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
@@ -11,10 +11,6 @@ function factory(propsData = {}) {
}
describe('~/vue_merge_request_widget/components/widget/action_buttons.vue', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('tertiaryButtons', () => {
it('renders buttons', () => {
factory({
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 973866176c2..5887670a58d 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
@@ -50,10 +50,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('on mount', () => {
it('fetches collapsed', async () => {
const fetchCollapsedData = jest
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
index 1bad5dacefa..785515ae846 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
@@ -25,10 +25,6 @@ describe('Deployment action button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when passed only icon via props', () => {
beforeEach(() => {
factory({
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 41df485b0de..1fdbbadf8b0 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
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import {
@@ -21,7 +21,7 @@ import {
retryDetails,
} from './deployment_mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -54,7 +54,6 @@ describe('DeploymentAction component', () => {
});
afterEach(() => {
- wrapper.destroy();
confirmAction.mockReset();
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
index 948d7ebab5e..77dac4204db 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
@@ -28,7 +28,6 @@ describe('~/vue_merge_request_widget/components/deployment/deployment_list.vue',
afterEach(() => {
wrapper?.destroy?.();
- wrapper = null;
});
describe('with few deployments', () => {
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
index f310f7669a9..74122f47ad3 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
@@ -32,10 +32,6 @@ describe('Deployment component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('always renders DeploymentInfo', () => {
expect(wrapper.findComponent(DeploymentInfo).exists()).toBe(true);
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
index 8994fa522d0..7a151c26934 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
@@ -28,10 +28,6 @@ describe('Deployment View App button', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink);
const findMrWigdetDeploymentDropdown = () => wrapper.findComponent(GlDropdown);
const findMrWigdetDeploymentDropdownIcon = () =>
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 548b68bc103..d2d622d0534 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
@@ -73,7 +73,6 @@ describe('Test report extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index 01049e54a7f..40158917f52 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
@@ -39,7 +39,6 @@ describe('Accessibility extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 67b327217ef..4b7870842bd 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -61,7 +61,6 @@ describe('Code Quality extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
index 13384e1efca..52a244107bd 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
@@ -48,7 +48,6 @@ describe('Terraform extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
index 015d394312a..20f1796008a 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
@@ -15,11 +15,6 @@ describe('MRWidgetHowToMerge', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(() => {
mountComponent();
});
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 f37276ad594..fad501ee7f5 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
@@ -1,9 +1,10 @@
import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -20,6 +21,7 @@ import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget/constants';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
@@ -28,6 +30,7 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
+import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
@@ -60,6 +63,8 @@ jest.mock('@sentry/browser', () => ({
Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
+ let stateQueryHandler;
+ let queryResponse;
let wrapper;
let mock;
@@ -83,37 +88,41 @@ describe('MrWidgetOptions', () => {
afterEach(() => {
mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
-
gl.mrWidgetData = {};
- gon.features = {};
});
- const createComponent = (mrData = mockData, options = {}) => {
- wrapper = mount(MrWidgetOptions, {
+ const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
+ const mounting = fullMount ? mount : shallowMount;
+
+ queryResponse = {
+ data: {
+ project: {
+ ...getStateQueryResponse.data.project,
+ mergeRequest: {
+ ...getStateQueryResponse.data.project.mergeRequest,
+ mergeError: mrData.mergeError || null,
+ },
+ },
+ },
+ };
+ stateQueryHandler = jest.fn().mockResolvedValue(queryResponse);
+ wrapper = mounting(MrWidgetOptions, {
propsData: {
mrData: { ...mrData },
},
data() {
- return { loading: false };
+ return {
+ loading: false,
+ ...data,
+ };
},
...options,
apolloProvider: createMockApollo([
- [
- getStateQuery,
- jest.fn().mockResolvedValue({
- data: {
- project: {
- ...getStateQueryResponse.data.project,
- mergeRequest: {
- ...getStateQueryResponse.data.project.mergeRequest,
- mergeError: mrData.mergeError || null,
- },
- },
- },
- }),
- ],
+ [approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)],
+ [getStateQuery, stateQueryHandler],
[readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)],
[
userPermissionsQuery,
@@ -351,18 +360,6 @@ describe('MrWidgetOptions', () => {
});
});
- describe('initPolling', () => {
- it('should call SmartInterval', () => {
- wrapper.vm.initPolling();
-
- expect(SmartInterval).toHaveBeenCalledWith(
- expect.objectContaining({
- callback: wrapper.vm.checkStatus,
- }),
- );
- });
- });
-
describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => {
wrapper.vm.initDeploymentsPolling();
@@ -529,23 +526,64 @@ describe('MrWidgetOptions', () => {
});
});
- describe('resumePolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- jest.spyOn(wrapper.vm.pollingInterval, 'resume').mockImplementation(() => {});
+ describe('Apollo query', () => {
+ const interval = 5;
+ const data = 'foo';
+ const mockCheckStatus = jest.fn().mockResolvedValue({ data });
+ const mockSetGraphqlData = jest.fn();
+ const mockSetData = jest.fn();
- wrapper.vm.resumePolling();
+ beforeEach(() => {
+ wrapper.destroy();
+
+ return createComponent(
+ mockData,
+ {},
+ {
+ pollInterval: interval,
+ startingPollInterval: interval,
+ mr: {
+ setData: mockSetData,
+ setGraphqlData: mockSetGraphqlData,
+ },
+ service: {
+ checkStatus: mockCheckStatus,
+ },
+ },
+ false,
+ );
+ });
- expect(wrapper.vm.pollingInterval.resume).toHaveBeenCalled();
+ describe('normal polling behavior', () => {
+ it('responds to the GraphQL query finishing', () => {
+ expect(mockSetGraphqlData).toHaveBeenCalledWith(queryResponse.data.project);
+ expect(mockCheckStatus).toHaveBeenCalled();
+ expect(mockSetData).toHaveBeenCalledWith(data, undefined);
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
+ });
});
- });
- describe('stopPolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- jest.spyOn(wrapper.vm.pollingInterval, 'stopTimer').mockImplementation(() => {});
+ describe('external event control', () => {
+ describe('enablePolling', () => {
+ it('enables the Apollo query polling using the event hub', () => {
+ eventHub.$emit('EnablePolling');
+
+ expect(stateQueryHandler).toHaveBeenCalled();
+ jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF);
+ expect(stateQueryHandler).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('disablePolling', () => {
+ it('disables the Apollo query polling using the event hub', () => {
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
- wrapper.vm.stopPolling();
+ eventHub.$emit('DisablePolling');
+ jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF);
- expect(wrapper.vm.pollingInterval.stopTimer).toHaveBeenCalled();
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1); // no additional polling after a real interval timeout
+ });
+ });
});
});
});
@@ -890,11 +928,7 @@ describe('MrWidgetOptions', () => {
});
describe('mock extension', () => {
- let pollRequest;
-
beforeEach(() => {
- pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
-
registerExtension(workingExtension());
createComponent();
@@ -945,10 +979,6 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.findComponent(GlButton).exists()).toBe(true);
expect(collapsedSection.findComponent(GlButton).text()).toBe('Full report');
});
-
- it('extension polling is not called if enablePolling flag is not passed', () => {
- expect(pollRequest).toHaveBeenCalledTimes(0);
- });
});
describe('expansion', () => {
@@ -1235,10 +1265,6 @@ describe('MrWidgetOptions', () => {
});
describe('widget container', () => {
- afterEach(() => {
- delete window.gon.features.refactorSecurityExtension;
- });
-
it('should not be displayed when the refactor_security_extension feature flag is turned off', () => {
createComponent();
expect(findWidgetContainer().exists()).toBe(false);
diff --git a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
index 88d9d0b4cff..a6288b9c725 100644
--- a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
@@ -20,7 +20,7 @@ describe('getStateKey', () => {
};
const bound = getStateKey.bind(context);
- expect(bound()).toEqual(null);
+ expect(bound()).toEqual('checking');
context.detailedMergeStatus = 'MERGEABLE';
diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
index 12c5c190e26..217103ab25c 100644
--- a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
@@ -32,10 +32,6 @@ describe('Alert Details Sidebar To Do', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]');
describe('updating the alert to do', () => {
diff --git a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
deleted file mode 100644
index ca9d4488870..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
+++ /dev/null
@@ -1,40 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`File row header component adds multiple ellipsises after 40 characters 1`] = `
-<div
- class="file-row-header bg-white sticky-top p-2 js-file-row-header"
- title="app/assets/javascripts/merge_requests/widget/diffs/notes"
->
- <gl-truncate-stub
- class="bold"
- position="middle"
- text="app/assets/javascripts/merge_requests/widget/diffs/notes"
- />
-</div>
-`;
-
-exports[`File row header component renders file path 1`] = `
-<div
- class="file-row-header bg-white sticky-top p-2 js-file-row-header"
- title="app/assets"
->
- <gl-truncate-stub
- class="bold"
- position="middle"
- text="app/assets"
- />
-</div>
-`;
-
-exports[`File row header component trucates path after 40 characters 1`] = `
-<div
- class="file-row-header bg-white sticky-top p-2 js-file-row-header"
- title="app/assets/javascripts/merge_requests"
->
- <gl-truncate-stub
- class="bold"
- position="middle"
- text="app/assets/javascripts/merge_requests"
- />
-</div>
-`;
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index f3fb840b270..8c2f2b52f8e 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -34,10 +34,6 @@ describe('Actions button component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.findComponent(GlButton);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/vue_shared/components/alert_details_table_spec.js b/spec/frontend/vue_shared/components/alert_details_table_spec.js
index 8a9ee4699bd..8e7a10c4d77 100644
--- a/spec/frontend/vue_shared/components/alert_details_table_spec.js
+++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js
@@ -41,11 +41,6 @@ describe('AlertDetails', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTableComponent = () => wrapper.findComponent(GlTable);
const findTableKeys = () => findTableComponent().findAll('tbody td:first-child');
const findTableFieldValueByKey = (fieldKey) =>
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index c7f9d8fd8d5..da5516f8db1 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -64,16 +64,7 @@ const REACTION_CONTROL_CLASSES = [
describe('vue_shared/components/awards_list', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createComponent = (props = {}) => {
- if (wrapper) {
- throw new Error('There should only be one wrapper created per test');
- }
-
wrapper = mount(AwardsList, { propsData: props });
};
const matchingEmojiTag = (name) => expect.stringMatching(`gl-emoji data-name="${name}"`);
@@ -98,7 +89,6 @@ describe('vue_shared/components/awards_list', () => {
addButtonClass: TEST_ADD_BUTTON_CLASS,
});
});
-
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index ce7fd40937f..6acd1f51a86 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -24,10 +24,6 @@ describe('Blob Rich Viewer component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the passed content without transformations', () => {
expect(wrapper.html()).toContain(content);
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 4b44311b253..a480e0869e8 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -23,10 +23,6 @@ describe('Blob Simple Viewer component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not fail if content is empty', () => {
const spy = jest.spyOn(window.console, 'error');
createComponent('');
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index ea708b6f3fe..d1b1e58f5d7 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -21,10 +21,6 @@ describe('Changed file icon', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findIcon = () => wrapper.findComponent(GlIcon);
const findIconName = () => findIcon().props('name');
const findIconClasses = () => findIcon().classes();
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
index 6932a812287..2a40511affb 100644
--- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
+++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
@@ -10,8 +10,6 @@ describe('vue_shared/components/chronic_duration_input', () => {
let hiddenElement;
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
textElement = null;
hiddenElement = null;
});
@@ -22,10 +20,6 @@ describe('vue_shared/components/chronic_duration_input', () => {
};
const createComponent = (props = {}) => {
- if (wrapper) {
- throw new Error('There should only be one wrapper created per test');
- }
-
wrapper = mount(ChronicDurationInput, { propsData: props });
findComponents();
};
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 4f24ec2d015..afb509b9fe6 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -82,10 +82,6 @@ describe('CI Badge Link Component', () => {
wrapper = shallowMount(CiBadgeLink, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
createComponent({ status: statuses[status] });
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 2064bee9673..31d63654168 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -7,11 +7,6 @@ describe('CI Icon component', () => {
const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render a span element with an svg', () => {
wrapper = shallowMount(ciIcon, {
propsData: {
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index b18b00e70bb..08a9c2a42d8 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -59,11 +59,6 @@ describe('clipboard button', () => {
expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('without gfm', () => {
beforeEach(() => {
createWrapper({
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index 31c08260dd0..584e29d94c4 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -21,11 +21,6 @@ describe('Clone Dropdown Button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
index 181692e61b5..25283eb1211 100644
--- a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -11,10 +11,6 @@ describe('Code Block Highlighted', () => {
wrapper = shallowMount(CodeBlock, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders highlighted code if language is supported', async () => {
createComponent({ code, language: 'javascript' });
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 9a4dbcc47ff..0fdfb96cb23 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -13,10 +13,6 @@ describe('Code Block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('overwrites the default slot', () => {
createComponent({}, { default: 'DEFAULT SLOT' });
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index 060048c4bbd..a839af3b709 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -34,10 +34,6 @@ describe('ColorPicker', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('label', () => {
it('hides the label if the label is not passed', () => {
createComponent(shallowMount);
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
index fe614f03119..700556edfd5 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
@@ -20,10 +20,6 @@ describe('ColorItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the correct title', () => {
expect(wrapper.text()).toBe(propsData.title);
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 5b0772f6e34..61a9ab3225e 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
@@ -13,7 +13,7 @@ import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -60,10 +60,6 @@ describe('LabelsSelectRoot', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
const defaultClasses = ['labels-select-wrapper', 'gl-relative'];
@@ -145,7 +141,7 @@ describe('LabelsSelectRoot', () => {
await waitForPromises();
});
- it('creates flash with error message', () => {
+ it('creates alert with error message', () => {
expect(createAlert).toHaveBeenCalledWith({
captureError: true,
message: 'Error fetching epic color.',
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
index 303824c77b3..914dfdbaab0 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
@@ -22,10 +22,6 @@ describe('DropdownContentsColorView', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findColors = () => wrapper.findAllComponents(ColorItem);
const findColorList = () => wrapper.findComponent(GlDropdownForm);
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
index ee4d3a2630a..c07faab20d0 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -28,10 +28,6 @@ describe('DropdownContent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findColorView = () => wrapper.findComponent(DropdownContentsColorView);
const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
const findDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
index d203d78477f..6c8aabe1c7f 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
@@ -15,10 +15,6 @@ describe('DropdownHeader', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
index 5bbdb136353..825f37c97e0 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -22,10 +22,6 @@ describe('DropdownValue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there is a color set', () => {
it('renders the color', () => {
expect(findColorItems()).toHaveLength(2);
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 1893e127f6f..62a2738d8df 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -24,10 +24,6 @@ describe('Commit component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a fork icon if it does not represent a tag', () => {
createComponent({
tag: false,
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index 3f7ec156c19..92cd7597637 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -1,14 +1,11 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-const createComponent = ({
- workspaceType = WorkspaceType.project,
- issuableType = TYPE_ISSUE,
-} = {}) =>
+const createComponent = ({ workspaceType = WORKSPACE_PROJECT, issuableType = TYPE_ISSUE } = {}) =>
shallowMount(ConfidentialityBadge, {
propsData: {
workspaceType,
@@ -23,14 +20,10 @@ describe('ConfidentialityBadge', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
- workspaceType | issuableType | expectedTooltip
- ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
- ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
+ workspaceType | issuableType | expectedTooltip
+ ${WORKSPACE_PROJECT} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
+ ${WORKSPACE_GROUP} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
index a660643d74f..d7f94c00d09 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -24,7 +24,7 @@ describe('Confirm Danger Modal', () => {
const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
const findPrimaryAction = () => findModal().props('actionPrimary');
const findCancelAction = () => findModal().props('actionCancel');
- const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr];
const createComponent = ({ provide = {} } = {}) =>
shallowMountExtended(ConfirmDangerModal, {
@@ -42,10 +42,6 @@ describe('Confirm Danger Modal', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the default warning message', () => {
expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING);
});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
index a179afccae0..379b5cde4d5 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -32,10 +32,6 @@ describe('Confirm Danger Modal', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the button', () => {
expect(wrapper.html()).toContain(buttonText);
});
diff --git a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
index 1cde92cf522..fbfef5cbe46 100644
--- a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
@@ -21,10 +21,6 @@ describe('vue_shared/components/confirm_fork_modal', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('visible = false', () => {
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
index c1e682a1aae..283ef52cee7 100644
--- a/spec/frontend/vue_shared/components/confirm_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -47,10 +47,6 @@ describe('vue_shared/components/confirm_modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModalStub);
const findForm = () => wrapper.find('form');
const findFormData = () =>
diff --git a/spec/frontend/vue_shared/components/content_transition_spec.js b/spec/frontend/vue_shared/components/content_transition_spec.js
index 8bb6d31cce7..5f2b1f096f3 100644
--- a/spec/frontend/vue_shared/components/content_transition_spec.js
+++ b/spec/frontend/vue_shared/components/content_transition_spec.js
@@ -13,11 +13,6 @@ const TEST_SLOTS = [
describe('~/vue_shared/components/content_transition.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(ContentTransition, {
propsData: {
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
index c1495e8264a..a3e5f187f9b 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
@@ -18,10 +18,6 @@ describe('DateTimePickerInput', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders label above the input', () => {
createComponent({
label: inputLabel,
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index aa41df438d2..5620b569409 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -26,10 +26,6 @@ describe('DateTimePicker', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders dropdown toggle button with selected text', async () => {
createComponent();
await nextTick();
diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
index 79001b9282f..dde2540e121 100644
--- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
+++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
@@ -17,10 +17,6 @@ describe('Deploy Board Instance', () => {
});
describe('as a non-canary deployment', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a div with the correct css status and tooltip data', () => {
wrapper = createComponent({
tooltipText: 'This is a pod',
@@ -43,10 +39,6 @@ describe('Deploy Board Instance', () => {
});
describe('as a canary deployment', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a div with canary class when stable prop is provided as false', async () => {
wrapper = createComponent({
stable: false,
@@ -58,10 +50,6 @@ describe('Deploy Board Instance', () => {
});
describe('as a legend item', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not have a tooltip', () => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
index 353d493add9..ca9c2b7d381 100644
--- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
+++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
@@ -16,10 +16,6 @@ describe('Design note pin component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should match the snapshot of note without index', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
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 99c973bdd26..930d2fc8cfe 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -110,10 +110,6 @@ describe('Diff Stats Dropdown', () => {
createComponent({ changed, added, deleted });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
expect(findChanged().props('text')).toBe(expectedDropdownHeader);
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 6e0717c29d7..694c69fbe9f 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -18,10 +18,6 @@ describe('DiffViewer', () => {
wrapper = mount(DiffViewer, { propsData });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders image diff', () => {
window.gon = {
relative_url_root: '',
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 16f924b44d8..7863ef45817 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -42,10 +42,6 @@ describe('ImageDiffViewer component', () => {
triggerEvent('mouseup', doc.body);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders image diff for replaced', () => {
createComponent(allProps);
const metaInfoElements = wrapper.findAll('.image-info');
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
index c4358f0d9cb..661db19ff0e 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
@@ -13,10 +13,6 @@ describe('Diff viewer mode changed component', () => {
});
});
- afterEach(() => {
- vm.destroy();
- });
-
it('renders aMode & bMode', () => {
expect(vm.text()).toContain('File mode changed from 123 to 321');
});
diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
index 8b1189f25d5..53e7d9fc7fc 100644
--- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
@@ -16,10 +16,6 @@ describe('vue_shared/components/dismissible_alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAlert = () => wrapper.findComponent(GlAlert);
describe('default', () => {
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index 7d8581e11e9..6d179434d1d 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -11,10 +11,6 @@ describe('DismissibleContainer', () => {
featureId: 'some-feature-id',
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
const findBtn = () => wrapper.find('[data-testid="close"]');
let mockAxios;
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
index 4b32fbffebe..b184ec4ac54 100644
--- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -23,11 +23,6 @@ describe('Dismissible Feedback Alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createFullComponent = () => createComponent({ mountFn: mount });
const findAlert = () => wrapper.findComponent(GlAlert);
diff --git a/spec/frontend/vue_shared/components/dom_element_listener_spec.js b/spec/frontend/vue_shared/components/dom_element_listener_spec.js
index a848c34b7ce..d31e9b867e4 100644
--- a/spec/frontend/vue_shared/components/dom_element_listener_spec.js
+++ b/spec/frontend/vue_shared/components/dom_element_listener_spec.js
@@ -42,10 +42,6 @@ describe('~/vue_shared/components/dom_element_listener.vue', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
index e34ed31b4bf..82130500458 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -11,10 +11,6 @@ describe('DropdownButton component', () => {
wrapper = mount(DropdownButton, { propsData: props, slots });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('dropdownToggleText', () => {
it('returns default toggle text', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index dd3e55c82bb..4dfee20764c 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -31,10 +31,6 @@ describe('DropdownWidget component', () => {
},
});
- // We need to mock out `showDropdown` which
- // invokes `show` method of BDropdown used inside GlDropdown.
- // Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
- jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
@@ -42,11 +38,6 @@ describe('DropdownWidget component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes default selectText prop to dropdown', () => {
expect(findDropdown().props('text')).toBe('Select');
});
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
index 119d6448507..ef42c17984a 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -39,10 +39,6 @@ describe('DropdownKeyboardNavigation', () => {
},
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('onInit', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js
index eef8b452f5f..217e795bc64 100644
--- a/spec/frontend/vue_shared/components/ensure_data_spec.js
+++ b/spec/frontend/vue_shared/components/ensure_data_spec.js
@@ -59,7 +59,6 @@ describe('EnsureData', () => {
});
afterEach(() => {
- wrapper.destroy();
Sentry.captureException.mockClear();
});
diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
index 57dce032d30..32ce2155494 100644
--- a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -63,11 +63,8 @@ describe('ProjectSelect', () => {
};
const openListbox = () => findListbox().vm.$emit('shown');
- beforeAll(() => {
- gon.api_version = apiVersion;
- });
-
beforeEach(() => {
+ gon.api_version = apiVersion;
mock = new MockAdapter(axios);
});
diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js
index 170c947e520..ad2a57d90eb 100644
--- a/spec/frontend/vue_shared/components/expand_button_spec.js
+++ b/spec/frontend/vue_shared/components/expand_button_spec.js
@@ -27,10 +27,6 @@ describe('Expand button', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the prepended collapse button', () => {
expect(expanderPrependEl().isVisible()).toBe(true);
expect(expanderAppendEl().isVisible()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index f0998b1b5c6..dce6c85b5b3 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -22,10 +22,6 @@ describe('File finder item spec', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders file name & path', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 0fcc0678c13..d95773f2218 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -16,10 +16,6 @@ describe('File Icon component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a span element and an icon', () => {
createComponent({
fileName: 'test.js',
diff --git a/spec/frontend/vue_shared/components/file_row_header_spec.js b/spec/frontend/vue_shared/components/file_row_header_spec.js
index 80f4c275dcc..885a80f73b5 100644
--- a/spec/frontend/vue_shared/components/file_row_header_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_header_spec.js
@@ -1,36 +1,24 @@
import { shallowMount } from '@vue/test-utils';
+import { GlTruncate } from '@gitlab/ui';
import FileRowHeader from '~/vue_shared/components/file_row_header.vue';
describe('File row header component', () => {
- let vm;
+ let wrapper;
function createComponent(path) {
- vm = shallowMount(FileRowHeader, {
+ wrapper = shallowMount(FileRowHeader, {
propsData: {
path,
},
});
}
- afterEach(() => {
- vm.destroy();
- });
-
it('renders file path', () => {
- createComponent('app/assets');
-
- expect(vm.element).toMatchSnapshot();
- });
-
- it('trucates path after 40 characters', () => {
- createComponent('app/assets/javascripts/merge_requests');
-
- expect(vm.element).toMatchSnapshot();
- });
-
- it('adds multiple ellipsises after 40 characters', () => {
- createComponent('app/assets/javascripts/merge_requests/widget/diffs/notes');
+ const path = 'app/assets';
+ createComponent(path);
- expect(vm.element).toMatchSnapshot();
+ const truncate = wrapper.findComponent(GlTruncate);
+ expect(truncate.exists()).toBe(true);
+ expect(truncate.props('text')).toBe(path);
});
});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index b70d4565f56..976866af27c 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -6,6 +6,9 @@ import FileIcon from '~/vue_shared/components/file_icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
+const scrollIntoViewMock = jest.fn();
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
describe('File row component', () => {
let wrapper;
@@ -18,10 +21,6 @@ describe('File row component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders name', () => {
const fileName = 't4';
createComponent({
@@ -72,11 +71,10 @@ describe('File row component', () => {
},
level: 0,
});
- jest.spyOn(wrapper.vm, '$emit');
wrapper.element.click();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', fileName);
+ expect(wrapper.emitted('toggleTreeOpen')[0][0]).toEqual(fileName);
});
it('calls scrollIntoView if made active', () => {
@@ -89,14 +87,12 @@ describe('File row component', () => {
level: 0,
});
- jest.spyOn(wrapper.vm, 'scrollIntoView');
-
wrapper.setProps({
file: { ...wrapper.props('file'), active: true },
});
return nextTick().then(() => {
- expect(wrapper.vm.scrollIntoView).toHaveBeenCalled();
+ expect(scrollIntoViewMock).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/vue_shared/components/file_tree_spec.js b/spec/frontend/vue_shared/components/file_tree_spec.js
index e8818e09dc0..9d2fa369910 100644
--- a/spec/frontend/vue_shared/components/file_tree_spec.js
+++ b/spec/frontend/vue_shared/components/file_tree_spec.js
@@ -33,10 +33,6 @@ describe('File Tree component', () => {
...pick(x.attributes(), Object.keys(TEST_EXTA_ARGS)),
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('file row component', () => {
beforeEach(() => {
createComponent({ file: {} });
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index b0e393bbf5e..123714353e2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -82,10 +82,6 @@ describe('FilteredSearchBarRoot', () => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('data', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 63c22aff3d5..dd0ec65c871 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
@@ -15,7 +15,7 @@ const labelsEndpoint = 'fake_labels_endpoint';
const groupEndpoint = 'fake_group_endpoint';
const projectEndpoint = 'fake_project_endpoint';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Filters actions', () => {
let state;
@@ -165,16 +165,10 @@ describe('Filters actions', () => {
});
describe('fetchAuthors', () => {
- let restoreVersion;
beforeEach(() => {
- restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
- afterEach(() => {
- gon.api_version = restoreVersion;
- });
-
describe('success', () => {
beforeEach(() => {
mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers);
@@ -305,17 +299,11 @@ describe('Filters actions', () => {
describe('fetchAssignees', () => {
describe('success', () => {
- let restoreVersion;
beforeEach(() => {
mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers);
- restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
- afterEach(() => {
- gon.api_version = restoreVersion;
- });
-
it('dispatches RECEIVE_ASSIGNEES_SUCCESS with received data and groupEndpoint set', () => {
return testAction(
actions.fetchAssignees,
@@ -350,17 +338,11 @@ describe('Filters actions', () => {
});
describe('error', () => {
- let restoreVersion;
beforeEach(() => {
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
- restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
- afterEach(() => {
- gon.api_version = restoreVersion;
- });
-
it('dispatches RECEIVE_ASSIGNEES_ERROR and groupEndpoint set', () => {
return testAction(
actions.fetchAssignees,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 164235e4bb9..9941abbfaea 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -120,10 +120,6 @@ describe('BaseToken', () => {
const getMockSuggestionListSuggestions = () =>
JSON.parse(findMockSuggestionList().attributes('data-suggestions'));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('data', () => {
it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 311d5a13280..a6bb32736db 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -9,14 +9,15 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockBranches, mockBranchToken } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
@@ -54,58 +55,83 @@ describe('BranchToken', () => {
let mock;
let wrapper;
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const triggerFetchBranches = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
describe('fetchBranches', () => {
- it('calls `config.fetchBranches` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches');
-
- wrapper.vm.fetchBranches('foo');
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchBranches: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
+ });
+ await nextTick();
- expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo');
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
});
- it('sets response to `branches` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches });
+ describe('when request is successful', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchBranches: jest.fn().mockResolvedValue({ data: mockBranches }),
+ },
+ });
+ });
+
+ it('calls `config.fetchBranches` with provided searchTerm param', async () => {
+ const searchTerm = 'foo';
+ await triggerFetchBranches(searchTerm);
- wrapper.vm.fetchBranches('foo');
+ expect(findBaseToken().props('config').fetchBranches).toHaveBeenCalledWith(searchTerm);
+ });
+
+ it('sets response to `branches`', async () => {
+ await triggerFetchBranches();
- return waitForPromises().then(() => {
- expect(wrapper.vm.branches).toEqual(mockBranches);
+ expect(findBaseToken().props('suggestions')).toEqual(mockBranches);
+ });
+
+ it('sets `loading` to false when request completes', async () => {
+ await triggerFetchBranches();
+
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchBranches: jest.fn().mockRejectedValue({}),
+ },
+ });
+ });
- wrapper.vm.fetchBranches('foo');
+ it('calls `createAlert` with alert error message when request fails', async () => {
+ await triggerFetchBranches();
- return waitForPromises().then(() => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching branches.',
});
});
- });
-
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
- wrapper.vm.fetchBranches('foo');
+ it('sets `loading` to false when request completes', async () => {
+ await triggerFetchBranches();
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
@@ -120,16 +146,13 @@ describe('BranchToken', () => {
await nextTick();
}
- beforeEach(async () => {
- wrapper = createComponent({ value: { data: mockBranches[0].name } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- branches: mockBranches,
+ beforeEach(() => {
+ wrapper = createComponent({
+ value: { data: mockBranches[0].name },
+ config: {
+ initialBranches: mockBranches,
+ },
});
-
- await nextTick();
});
it('renders gl-filtered-search-token component', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index 7be7035a0f2..ce134f7d24e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -22,7 +22,7 @@ import {
mockProjectCrmContactsQueryResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
@@ -79,7 +79,6 @@ describe('CrmContactToken', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -159,7 +158,7 @@ describe('CrmContactToken', () => {
});
});
- it('calls `createAlert` with flash error message when request fails', async () => {
+ it('calls `createAlert` with alert error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index ecd3e8a04f1..8526631c63d 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -22,7 +22,7 @@ import {
mockProjectCrmOrganizationsQueryResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
@@ -78,7 +78,6 @@ describe('CrmOrganizationToken', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -158,7 +157,7 @@ describe('CrmOrganizationToken', () => {
});
});
- it('calls `createAlert` with flash error message when request fails', async () => {
+ it('calls `createAlert` with alert error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 773df01ada7..4e00b6837a3 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
@@ -17,10 +17,11 @@ import {
OPTIONS_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockReactionEmojiToken, mockEmojis } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const GlEmoji = { template: '<img/>' };
const defaultStubs = {
Portal: true,
@@ -60,58 +61,72 @@ describe('EmojiToken', () => {
let mock;
let wrapper;
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const triggerFetchEmojis = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
describe('fetchEmojis', () => {
- it('calls `config.fetchEmojis` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis');
-
- wrapper.vm.fetchEmojis('foo');
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchEmojis: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
+ });
+ await nextTick();
- expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo');
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
});
- it('sets response to `emojis` when request is successful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis);
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
- wrapper.vm.fetchEmojis('foo');
+ beforeEach(async () => {
+ wrapper = createComponent({
+ config: {
+ fetchEmojis: jest.fn().mockResolvedValue({ data: mockEmojis }),
+ },
+ });
+ return triggerFetchEmojis(searchTerm);
+ });
- return waitForPromises().then(() => {
- expect(wrapper.vm.emojis).toEqual(mockEmojis);
+ it('calls `config.fetchEmojis` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchEmojis).toHaveBeenCalledWith(searchTerm);
});
- });
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
+ it('sets response to `emojis`', () => {
+ expect(findBaseToken().props('suggestions')).toEqual(mockEmojis);
+ });
+ });
- wrapper.vm.fetchEmojis('foo');
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchEmojis: jest.fn().mockRejectedValue({}),
+ },
+ });
+ return triggerFetchEmojis();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert` with alert error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching emojis.',
});
});
- });
-
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
-
- wrapper.vm.fetchEmojis('foo');
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
@@ -123,15 +138,10 @@ describe('EmojiToken', () => {
beforeEach(async () => {
wrapper = createComponent({
value: { data: `"${mockEmojis[0].name}"` },
+ config: {
+ initialEmojis: mockEmojis,
+ },
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- emojis: mockEmojis,
- });
-
- await nextTick();
});
it('renders gl-filtered-search-token component', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 9d96123c17f..b9275409125 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -11,7 +11,7 @@ import {
mockRegularLabel,
mockLabels,
} from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -20,7 +20,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import { mockLabelToken } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
BaseToken,
@@ -60,103 +60,125 @@ function createComponent(options = {}) {
describe('LabelToken', () => {
let mock;
let wrapper;
+ const defaultLabels = OPTIONS_NONE_ANY;
beforeEach(() => {
mock = new MockAdapter(axios);
});
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const findSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findTokenSegments = () => wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const triggerFetchLabels = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
describe('getActiveLabel', () => {
it('returns label object from labels array based on provided `currentValue` param', () => {
- expect(wrapper.vm.getActiveLabel(mockLabels, 'Foo Label')).toEqual(mockRegularLabel);
+ wrapper = createComponent();
+
+ expect(findBaseToken().props('getActiveTokenValue')(mockLabels, 'Foo Label')).toEqual(
+ mockRegularLabel,
+ );
});
});
describe('getLabelName', () => {
- it('returns value of `name` or `title` property present in provided label param', () => {
- let mockLabel = {
- title: 'foo',
- };
+ it('returns value of `name` or `title` property present in provided label param', async () => {
+ const customMockLabels = [
+ { title: 'Title with no name label' },
+ { name: 'Name Label', title: 'Title with name label' },
+ ];
+
+ wrapper = createComponent({
+ active: true,
+ config: {
+ ...mockLabelToken,
+ fetchLabels: jest.fn().mockResolvedValue({ data: customMockLabels }),
+ },
+ stubs: { Portal: true },
+ });
- expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.title);
+ await waitForPromises();
- mockLabel = {
- name: 'foo',
- };
+ const suggestions = findSuggestions();
+ const indexWithTitle = defaultLabels.length;
+ const indexWithName = defaultLabels.length + 1;
- expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.name);
+ expect(suggestions.at(indexWithTitle).text()).toBe(customMockLabels[0].title);
+ expect(suggestions.at(indexWithName).text()).toBe(customMockLabels[1].name);
});
});
describe('fetchLabels', () => {
- it('calls `config.fetchLabels` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels');
-
- wrapper.vm.fetchLabels('foo');
-
- expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo');
- });
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
+
+ beforeEach(async () => {
+ wrapper = createComponent({
+ config: {
+ fetchLabels: jest.fn().mockResolvedValue({ data: mockLabels }),
+ },
+ });
+ await triggerFetchLabels(searchTerm);
+ });
- it('sets response to `labels` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels);
+ it('calls `config.fetchLabels` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchLabels).toHaveBeenCalledWith(searchTerm);
+ });
- wrapper.vm.fetchLabels('foo');
+ it('sets response to `labels`', () => {
+ expect(findBaseToken().props('suggestions')).toEqual(mockLabels);
+ });
- return waitForPromises().then(() => {
- expect(wrapper.vm.labels).toEqual(mockLabels);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
-
- wrapper.vm.fetchLabels('foo');
+ describe('when request fails', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ config: {
+ fetchLabels: jest.fn().mockRejectedValue({}),
+ },
+ });
+ await triggerFetchLabels();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert` with alert error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching labels.',
});
});
- });
-
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
-
- wrapper.vm.fetchLabels('foo');
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
});
describe('template', () => {
- const defaultLabels = OPTIONS_NONE_ANY;
-
beforeEach(async () => {
- wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- labels: mockLabels,
+ wrapper = createComponent({
+ value: { data: `"${mockRegularLabel.title}"` },
+ config: {
+ initialLabels: mockLabels,
+ },
});
await nextTick();
});
it('renders base-token component', () => {
- const baseTokenEl = wrapper.findComponent(BaseToken);
+ const baseTokenEl = findBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
@@ -166,7 +188,7 @@ describe('LabelToken', () => {
});
it('renders token item when value is selected', () => {
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label"
expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label"
@@ -181,12 +203,12 @@ describe('LabelToken', () => {
config: { ...mockLabelToken, defaultLabels },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const suggestions = findSuggestions();
expect(suggestions).toHaveLength(defaultLabels.length);
defaultLabels.forEach((label, index) => {
@@ -200,7 +222,7 @@ describe('LabelToken', () => {
config: { ...mockLabelToken, defaultLabels: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
@@ -215,11 +237,10 @@ describe('LabelToken', () => {
config: { ...mockLabelToken },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
-
- const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const suggestions = findSuggestions();
expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
OPTIONS_NONE_ANY.forEach((label, index) => {
@@ -234,7 +255,7 @@ describe('LabelToken', () => {
input: mockInput,
},
});
- wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+ findBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index 589697fe542..fea1496a80b 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -8,16 +8,17 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/milestones/utils');
const defaultStubs = {
@@ -57,6 +58,12 @@ describe('MilestoneToken', () => {
let mock;
let wrapper;
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const triggerFetchMilestones = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent();
@@ -64,73 +71,77 @@ describe('MilestoneToken', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
describe('fetchMilestones', () => {
- describe('when config.shouldSkipSort is true', () => {
- beforeEach(() => {
- wrapper.vm.config.shouldSkipSort = true;
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchMilestones: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
});
+ await nextTick();
- afterEach(() => {
- wrapper.vm.config.shouldSkipSort = false;
- });
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
+ });
+
+ describe('when config.shouldSkipSort is true', () => {
it('does not call sortMilestonesByDueDate', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
- data: mockMilestones,
+ wrapper = createComponent({
+ config: {
+ shouldSkipSort: true,
+ fetchMilestones: jest.fn().mockResolvedValue({ data: mockMilestones }),
+ },
});
- wrapper.vm.fetchMilestones();
-
- await waitForPromises();
+ await triggerFetchMilestones();
expect(sortMilestonesByDueDate).toHaveBeenCalledTimes(0);
});
});
- it('calls `config.fetchMilestones` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones');
-
- wrapper.vm.fetchMilestones('foo');
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
- expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo');
- });
-
- it('sets response to `milestones` when request is successful', () => {
- wrapper.vm.config.shouldSkipSort = false;
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ shouldSkipSort: false,
+ fetchMilestones: jest.fn().mockResolvedValue({ data: mockMilestones }),
+ },
+ });
+ return triggerFetchMilestones(searchTerm);
+ });
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
- data: mockMilestones,
+ it('calls `config.fetchMilestones` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchMilestones).toHaveBeenCalledWith(searchTerm);
});
- wrapper.vm.fetchMilestones();
- return waitForPromises().then(() => {
- expect(wrapper.vm.milestones).toEqual(mockMilestones);
+ it('sets response to `milestones`', () => {
expect(sortMilestonesByDueDate).toHaveBeenCalled();
+ expect(findBaseToken().props('suggestions')).toEqual(mockMilestones);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
-
- wrapper.vm.fetchMilestones('foo');
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchMilestones: jest.fn().mockRejectedValue({}),
+ },
+ });
+ return triggerFetchMilestones();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert` with alert error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
});
- });
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
-
- wrapper.vm.fetchMilestones('foo');
-
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
@@ -143,15 +154,12 @@ describe('MilestoneToken', () => {
];
beforeEach(async () => {
- wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- milestones: mockMilestones,
+ wrapper = createComponent({
+ value: { data: `"${mockRegularMilestone.title}"` },
+ config: {
+ initialMilestones: mockMilestones,
+ },
});
-
- await nextTick();
});
it('renders gl-filtered-search-token component', () => {
@@ -228,7 +236,7 @@ describe('MilestoneToken', () => {
it('finds the correct value from the activeToken', () => {
DEFAULT_MILESTONES.forEach(({ value, title }) => {
- const activeToken = wrapper.vm.getActiveMilestone([], value);
+ const activeToken = findBaseToken().props('getActiveTokenValue')([], value);
expect(activeToken.title).toEqual(title);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 0e5fa0f66d4..5190ab919b1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -2,11 +2,11 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import { mockReleaseToken } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ReleaseToken', () => {
const id = '123';
@@ -27,10 +27,6 @@ describe('ReleaseToken', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders release value', async () => {
wrapper = createComponent({ value: { data: id } });
await nextTick();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index 32cb74d5f80..89003296854 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -17,7 +17,7 @@ import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_t
import { mockAuthorToken, mockUsers } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
@@ -67,99 +67,82 @@ function createComponent(options = {}) {
}
describe('UserToken', () => {
- const originalGon = window.gon;
const currentUserLength = 1;
let mock;
let wrapper;
- const getBaseToken = () => wrapper.findComponent(BaseToken);
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
- window.gon = originalGon;
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
describe('fetchUsers', () => {
+ const triggerFetchUsers = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
wrapper = createComponent();
});
- it('calls `config.fetchUsers` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers');
-
- getBaseToken().vm.$emit('fetch-suggestions', mockUsers[0].username);
-
- expect(wrapper.vm.config.fetchUsers).toHaveBeenCalledWith(
- mockAuthorToken.fetchPath,
- mockUsers[0].username,
- );
- });
-
- it('sets response to `users` when request is successful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers').mockResolvedValue(mockUsers);
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
-
- return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchUsers: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
});
+ await nextTick();
+
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
});
- // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
- describe('when there are null users presents', () => {
- const mockUsersWithNullUser = mockUsers.concat([null]);
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
beforeEach(() => {
- jest
- .spyOn(wrapper.vm.config, 'fetchUsers')
- .mockResolvedValue({ data: mockUsersWithNullUser });
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ wrapper = createComponent({
+ config: {
+ fetchUsers: jest.fn().mockResolvedValue({ data: mockUsers }),
+ },
+ });
+ return triggerFetchUsers(searchTerm);
});
- describe('when res.data is present', () => {
- it('filters the successful response when null values are present', () => {
- return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
- });
- });
+ it('calls `config.fetchUsers` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchUsers).toHaveBeenCalledWith(searchTerm);
});
- describe('when response is an array', () => {
- it('filters the successful response when null values are present', () => {
- return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
- });
- });
+ it('sets response to `users` when request is successful', () => {
+ expect(findBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchUsers: jest.fn().mockRejectedValue({}),
+ },
+ });
+ return triggerFetchUsers();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert` with alert error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
- });
-
- it('sets `loading` to false when request completes', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
- await waitForPromises();
-
- expect(getBaseToken().props('suggestionsLoading')).toBe(false);
+ it('sets `loading` to false when request completes', async () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
+ });
});
});
});
@@ -178,12 +161,12 @@ describe('UserToken', () => {
data: { users: mockUsers },
});
- const baseTokenEl = getBaseToken();
+ const baseTokenEl = findBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
suggestions: mockUsers,
- getActiveTokenValue: wrapper.vm.getActiveUser,
+ getActiveTokenValue: baseTokenEl.props('getActiveTokenValue'),
});
});
@@ -191,7 +174,6 @@ describe('UserToken', () => {
wrapper = createComponent({
value: { data: mockUsers[0].username },
data: { users: mockUsers },
- stubs: { Portal: true },
});
await nextTick();
@@ -215,30 +197,13 @@ describe('UserToken', () => {
users: [
{
...mockUsers[0],
+ avatarUrl: mockUsers[0].avatar_url,
+ avatar_url: undefined,
},
],
},
- stubs: { Portal: true },
});
- await nextTick();
-
- expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- users: [
- {
- ...mockUsers[0],
- avatarUrl: mockUsers[0].avatar_url,
- avatar_url: undefined,
- },
- ],
- });
-
- await nextTick();
-
expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
});
@@ -264,7 +229,6 @@ describe('UserToken', () => {
wrapper = createComponent({
active: true,
config: { ...mockAuthorToken, defaultUsers: [] },
- stubs: { Portal: true },
});
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
diff --git a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
index 361b162b6a0..eee8a0c4532 100644
--- a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
+++ b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
@@ -10,10 +10,6 @@ describe('Form Footer Actions', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders content properly', () => {
const defaultSlot = 'Foo';
const prepend = 'Bar';
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index e1da8b690af..4f1603f93ba 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -10,10 +10,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
describe('InputCopyToggleVisibility', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const valueProp = 'hR8x1fuJbzwu5uFKLf9e';
const createComponent = (options = {}) => {
@@ -21,7 +17,7 @@ describe('InputCopyToggleVisibility', () => {
InputCopyToggleVisibility,
merge({}, options, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
}),
);
diff --git a/spec/frontend/vue_shared/components/form/title_spec.js b/spec/frontend/vue_shared/components/form/title_spec.js
index 452f3723e76..d499f847c72 100644
--- a/spec/frontend/vue_shared/components/form/title_spec.js
+++ b/spec/frontend/vue_shared/components/form/title_spec.js
@@ -12,10 +12,6 @@ describe('Title edit field', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index af53d256236..1de206123fe 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -10,10 +10,6 @@ describe('GlCountdown', () => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there is time remaining', () => {
beforeEach(async () => {
wrapper = mount(GlCountdown, {
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 458f2cc5374..da9bc0f8a2f 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -48,11 +48,6 @@ describe('Header CI Component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('render', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline' });
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 77c03dc0c3c..76e66d07fa0 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -23,10 +23,6 @@ describe('HelpPopover', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with title and content', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js
index c63e46313b3..dd20b09f176 100644
--- a/spec/frontend/vue_shared/components/integration_help_text_spec.js
+++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js
@@ -22,11 +22,6 @@ describe('IntegrationHelpText component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should use the gl components', () => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js
index 10c6cbe6d94..f69a883ee4d 100644
--- a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js
+++ b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js
@@ -37,10 +37,6 @@ describe('~/vue_shared/components/keep_alive_slots.vue', () => {
isVisible: x.isVisible(),
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index 7ed6a59c844..4e83f3e1c06 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup, GlListbox } from '@gitlab/ui';
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
describe('ListboxInput', () => {
@@ -27,7 +27,7 @@ describe('ListboxInput', () => {
// Finders
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
- const findGlListbox = () => wrapper.findComponent(GlListbox);
+ const findGlListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findInput = () => wrapper.find('input');
const createComponent = (propsData) => {
@@ -153,7 +153,7 @@ describe('ListboxInput', () => {
expect(findGlListbox().props('searchable')).toBe(true);
});
- it('passes all items to GlListbox by default', () => {
+ it('passes all items to GlCollapsibleListbox by default', () => {
createComponent();
expect(findGlListbox().props('items')).toStrictEqual(items);
diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
index a80717a1aea..1c7f419b118 100644
--- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js
+++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
@@ -17,7 +17,6 @@ describe('Local Storage Sync', () => {
const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value);
afterEach(() => {
- wrapper.destroy();
localStorage.clear();
});
diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
index ecb2b37c3a5..8aab867f32a 100644
--- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
@@ -17,11 +17,6 @@ describe('Apply Suggestion component', () => {
beforeEach(() => createWrapper());
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('initial template', () => {
it('renders a dropdown with the correct props', () => {
const dropdown = findDropdown();
diff --git a/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js
new file mode 100644
index 00000000000..67f296b1bf0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js
@@ -0,0 +1,66 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
+import { launchDrawioEditor } from '~/drawio/drawio_editor';
+import { create } from '~/drawio/markdown_field_editor_facade';
+
+jest.mock('~/drawio/drawio_editor');
+jest.mock('~/drawio/markdown_field_editor_facade');
+
+describe('vue_shared/components/markdown/drawio_toolbar_button', () => {
+ let wrapper;
+ let textArea;
+ const uploadsPath = '/uploads';
+ const markdownPreviewPath = '/markdown/preview';
+
+ const buildWrapper = (props = { uploadsPath, markdownPreviewPath }) => {
+ wrapper = shallowMount(DrawioToolbarButton, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ textArea = document.createElement('textarea');
+ textArea.classList.add('js-gfm-input');
+
+ document.body.appendChild(textArea);
+ });
+
+ afterEach(() => {
+ textArea.remove();
+ });
+
+ describe('default', () => {
+ it('renders button that launches draw.io editor', () => {
+ buildWrapper();
+
+ expect(wrapper.findComponent(GlButton).props()).toMatchObject({
+ icon: 'diagram',
+ category: 'tertiary',
+ });
+ });
+ });
+
+ describe('when clicking button', () => {
+ it('launches draw.io editor', async () => {
+ const editorFacadeStub = {};
+
+ create.mockReturnValueOnce(editorFacadeStub);
+
+ buildWrapper();
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(create).toHaveBeenCalledWith({
+ markdownPreviewPath,
+ textArea,
+ uploadsPath,
+ });
+ expect(launchDrawioEditor).toHaveBeenCalledWith({
+ editorFacade: editorFacadeStub,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
index 34071775b9c..becd4257cbe 100644
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
@@ -21,14 +21,10 @@ describe('vue_shared/component/markdown/editor_mode_dropdown', () => {
.filter((item) => item.text().startsWith(text))
.at(0);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
- modeText | value | dropdownText | otherMode
- ${'Rich text'} | ${'richText'} | ${'View markdown'} | ${'Markdown'}
- ${'Markdown'} | ${'markdown'} | ${'View rich text'} | ${'Rich text'}
+ modeText | value | dropdownText | otherMode
+ ${'Rich text'} | ${'richText'} | ${'Viewing rich text'} | ${'Markdown'}
+ ${'Markdown'} | ${'markdown'} | ${'Viewing markdown'} | ${'Rich text'}
`('$modeText', ({ modeText, value, dropdownText, otherMode }) => {
beforeEach(() => {
createComponent({ value });
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
index 176ccfc5a69..1bbbe0896f2 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -6,20 +6,14 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
describe('Markdown Field View component', () => {
- let wrapper;
-
function createComponent() {
- wrapper = shallowMount(MarkdownFieldView);
+ shallowMount(MarkdownFieldView);
}
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('processes rendering with GFM', () => {
expect(renderGFM).toHaveBeenCalledTimes(1);
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index ed417097e1e..68f05e5119d 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import { GlTabs } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
+import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Markdown field header component', () => {
@@ -26,6 +27,7 @@ describe('Markdown field header component', () => {
findToolbarButtons()
.filter((button) => button.props(prop) === value)
.at(0);
+ const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton);
beforeEach(() => {
window.gl = {
@@ -37,10 +39,6 @@ describe('Markdown field header component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('markdown header buttons', () => {
it('renders the buttons with the correct title', () => {
const buttons = [
@@ -197,4 +195,24 @@ describe('Markdown field header component', () => {
expect(findToolbarButtons().length).toBe(defaultCount);
});
});
+
+ describe('when drawIOEnabled is true', () => {
+ const uploadsPath = '/uploads';
+ const markdownPreviewPath = '/preview';
+
+ beforeEach(() => {
+ createWrapper({
+ drawioEnabled: true,
+ uploadsPath,
+ markdownPreviewPath,
+ });
+ });
+
+ it('renders drawio toolbar button', () => {
+ expect(findDrawioToolbarButton().props()).toEqual({
+ uploadsPath,
+ markdownPreviewPath,
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 26b536984ff..681ff6c8dd3 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -1,4 +1,5 @@
import axios from 'axios';
+import Autosize from 'autosize';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -9,10 +10,15 @@ import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { stubComponent } from 'helpers/stub_component';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji');
+jest.mock('autosize');
describe('vue_shared/component/markdown/markdown_editor', () => {
+ useLocalStorageSpy();
+
let wrapper;
const value = 'test markdown';
const renderMarkdownPath = '/api/markdown';
@@ -57,14 +63,27 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findContentEditor = () => wrapper.findComponent(ContentEditor);
+ const enableContentEditor = async () => {
+ findMarkdownField().vm.$emit('enableContentEditor');
+ await nextTick();
+ await waitForPromises();
+ };
+
+ const enableMarkdownEditor = async () => {
+ findContentEditor().vm.$emit('enableMarkdownEditor');
+ await nextTick();
+ await waitForPromises();
+ };
+
beforeEach(() => {
window.uploads_path = 'uploads';
mock = new MockAdapter(axios);
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
+
+ localStorage.clear();
});
it('displays markdown field by default', () => {
@@ -83,8 +102,133 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
+ it('enables content editor switcher when contentEditorEnabled prop is true', () => {
+ buildWrapper({ propsData: { enableContentEditor: true } });
+
+ expect(findMarkdownField().text()).toContain('Rich text');
+ });
+
+ it('hides content editor switcher when contentEditorEnabled prop is false', () => {
+ buildWrapper({ propsData: { enableContentEditor: false } });
+
+ expect(findMarkdownField().text()).not.toContain('Rich text');
+ });
+
+ it('passes down any additional props to markdown field component', () => {
+ const propsData = {
+ line: { text: 'hello world', richText: 'hello world' },
+ lines: [{ text: 'hello world', richText: 'hello world' }],
+ canSuggest: true,
+ };
+
+ buildWrapper({
+ propsData: { ...propsData, myCustomProp: 'myCustomValue', 'data-testid': 'custom id' },
+ });
+
+ expect(findMarkdownField().props()).toMatchObject(propsData);
+ expect(findMarkdownField().vm.$attrs).toMatchObject({
+ myCustomProp: 'myCustomValue',
+
+ // data-testid isn't copied over
+ 'data-testid': 'markdown-field',
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables markdown field when disabled prop is true', () => {
+ buildWrapper({ propsData: { disabled: true } });
+
+ expect(findMarkdownField().find('textarea').attributes('disabled')).toBe('disabled');
+ });
+
+ it('enables markdown field when disabled prop is false', () => {
+ buildWrapper({ propsData: { disabled: false } });
+
+ expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined);
+ });
+
+ it('disables content editor when disabled prop is true', async () => {
+ buildWrapper({ propsData: { disabled: true } });
+
+ await enableContentEditor();
+
+ expect(findContentEditor().props('editable')).toBe(false);
+ });
+
+ it('enables content editor when disabled prop is false', async () => {
+ buildWrapper({ propsData: { disabled: false } });
+
+ await enableContentEditor();
+
+ expect(findContentEditor().props('editable')).toBe(true);
+ });
+ });
+
+ describe('autosize', () => {
+ it('autosizes the textarea when the value changes', async () => {
+ buildWrapper();
+ await findTextarea().setValue('Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines');
+
+ expect(Autosize.update).toHaveBeenCalled();
+ });
+
+ it('autosizes the textarea when the value changes from outside the component', async () => {
+ buildWrapper();
+ wrapper.setProps({ value: 'Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines' });
+
+ await nextTick();
+ await waitForPromises();
+ expect(Autosize.update).toHaveBeenCalled();
+ });
+
+ it('does not autosize the textarea if markdown editor is disabled', async () => {
+ buildWrapper();
+ await enableContentEditor();
+
+ wrapper.setProps({ value: 'Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines' });
+
+ expect(Autosize.update).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('autosave', () => {
+ it('automatically saves the textarea value to local storage if autosaveKey is defined', () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: 'This is **markdown**' } });
+
+ expect(localStorage.getItem('autosave/issue/1234')).toBe('This is **markdown**');
+ });
+
+ it("loads value from local storage if autosaveKey is defined, and value isn't", () => {
+ localStorage.setItem('autosave/issue/1234', 'This is **markdown**');
+
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } });
+
+ expect(findTextarea().element.value).toBe('This is **markdown**');
+ });
+
+ it("doesn't load value from local storage if autosaveKey is defined, and value is", () => {
+ localStorage.setItem('autosave/issue/1234', 'This is **markdown**');
+
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+
+ expect(findTextarea().element.value).toBe('test markdown');
+ });
+
+ it('does not save the textarea value to local storage if autosaveKey is not defined', () => {
+ buildWrapper({ propsData: { value: 'This is **markdown**' } });
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('does not save the textarea value to local storage if value is empty', () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } });
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
it('renders markdown field textarea', () => {
- buildWrapper();
+ buildWrapper({ propsData: { supportsQuickActions: true } });
expect(findTextarea().attributes()).toEqual(
expect.objectContaining({
@@ -92,6 +236,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
name: formFieldName,
placeholder: formFieldPlaceholder,
'aria-label': formFieldAriaLabel,
+ 'data-supports-quick-actions': 'true',
}),
);
@@ -107,9 +252,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();
- findMarkdownField().vm.$emit('enableContentEditor');
-
- await nextTick();
+ await enableContentEditor();
expect(wrapper.emitted(EDITING_MODE_CONTENT_EDITOR)).toHaveLength(1);
});
@@ -119,11 +262,8 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
stubs: { ContentEditor: stubComponent(ContentEditor) },
});
- findMarkdownField().vm.$emit('enableContentEditor');
-
- await nextTick();
-
- findContentEditor().vm.$emit('enableMarkdownEditor');
+ await enableContentEditor();
+ await enableMarkdownEditor();
expect(wrapper.emitted(EDITING_MODE_MARKDOWN_FIELD)).toHaveLength(1);
});
@@ -138,6 +278,16 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
+ it('autosaves the markdown value to local storage', async () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+
+ const newValue = 'new value';
+
+ await findTextarea().setValue(newValue);
+
+ expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
+ });
+
describe('when autofocus is true', () => {
beforeEach(async () => {
buildWrapper({ attachTo: document.body, propsData: { autofocus: true } });
@@ -159,9 +309,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
describe(`when markdown field triggers enableContentEditor event`, () => {
- beforeEach(() => {
+ beforeEach(async () => {
buildWrapper();
- findMarkdownField().vm.$emit('enableContentEditor');
+ await enableContentEditor();
});
it('displays the content editor', () => {
@@ -169,7 +319,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect.objectContaining({
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
- useBottomToolbar: false,
markdown: value,
}),
);
@@ -198,9 +347,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
- beforeEach(() => {
- buildWrapper();
- findMarkdownField().vm.$emit('enableContentEditor');
+ beforeEach(async () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+ await enableContentEditor();
});
describe('when autofocus is true', () => {
@@ -224,6 +373,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
+ it('autosaves the content editor value to local storage', async () => {
+ const newValue = 'new value';
+
+ await findContentEditor().vm.$emit('change', { markdown: newValue });
+
+ expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
+ });
+
it('bubbles up keydown event', () => {
const event = new Event('keydown');
@@ -233,9 +390,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
describe(`when richText editor triggers enableMarkdownEditor event`, () => {
- beforeEach(() => {
- findContentEditor().vm.$emit('enableMarkdownEditor');
- });
+ beforeEach(enableMarkdownEditor);
it('hides the content editor', () => {
expect(findContentEditor().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js
new file mode 100644
index 00000000000..8ad9ad30c1d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import SavedRepliesDropdown from '~/vue_shared/components/markdown/saved_replies_dropdown.vue';
+import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
+
+let wrapper;
+let savedRepliesResp;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ savedRepliesResp = jest.fn().mockResolvedValue(response);
+
+ const requestHandlers = [[savedRepliesQuery, savedRepliesResp]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mountExtended(SavedRepliesDropdown, {
+ propsData: {
+ newSavedRepliesPath: '/new',
+ },
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Saved replies dropdown', () => {
+ it('fetches data when dropdown gets opened', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ wrapper.findByTestId('saved-replies-dropdown-toggle').trigger('click');
+
+ await waitForPromises();
+
+ expect(savedRepliesResp).toHaveBeenCalled();
+ });
+
+ it('adds markdown toolbar attributes to dropdown items', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ wrapper.findByTestId('saved-replies-dropdown-toggle').trigger('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findByTestId('saved-reply-dropdown-item').attributes()).toEqual(
+ expect.objectContaining({
+ 'data-md-cursor-offset': '0',
+ 'data-md-prepend': 'true',
+ 'data-md-tag': 'Saved Reply Content',
+ }),
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index 9db1b779a04..9768bc7a6dd 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -25,7 +25,7 @@ describe('Suggestion Diff component', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -34,10 +34,6 @@ describe('Suggestion Diff component', () => {
window.gon.current_user_id = 1;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findApplyButton = () => wrapper.findComponent(ApplySuggestion);
const findApplyBatchButton = () => wrapper.find('.js-apply-batch-btn');
const findAddToBatchButton = () => wrapper.find('.js-add-to-batch-btn');
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
index f9a8b64f89b..c46a2d3e117 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
@@ -36,10 +36,6 @@ describe('SuggestionDiffRow', () => {
const findNewLineWrapper = () => wrapper.find('.new_line');
const findSuggestionContent = () => wrapper.find('[data-testid="suggestion-diff-content"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders correctly', () => {
it('renders the correct base suggestion markup', () => {
factory({
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
index d84483c1663..8c7f51664ad 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -61,11 +61,6 @@ describe('Suggestion Diff component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
index 82210e79799..33e9d6add99 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -20,11 +20,6 @@ describe('toolbar_button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const getButtonShortcutsAttr = () => {
return wrapper.findComponent(GlButton).attributes('data-md-shortcuts');
};
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index b1a1dbbeb7a..fea14f80496 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -11,10 +11,6 @@ describe('toolbar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('user can attach file', () => {
beforeEach(() => {
createMountedWrapper();
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
index 2b311b75f85..37b0767616a 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -36,8 +36,6 @@ describe('MarkdownDrawer', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
Object.keys(cache).forEach((key) => delete cache[key]);
});
diff --git a/spec/frontend/vue_shared/components/memory_graph_spec.js b/spec/frontend/vue_shared/components/memory_graph_spec.js
index ae8d5ff78ba..81325fb3269 100644
--- a/spec/frontend/vue_shared/components/memory_graph_spec.js
+++ b/spec/frontend/vue_shared/components/memory_graph_spec.js
@@ -1,10 +1,8 @@
import { GlSparklineChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
describe('MemoryGraph', () => {
- const Component = Vue.extend(MemoryGraph);
let wrapper;
const metrics = [
[1573586253.853, '2.87'],
@@ -13,12 +11,10 @@ describe('MemoryGraph', () => {
[1573586433.853, '3.0066964285714284'],
];
- afterEach(() => {
- wrapper.destroy();
- });
+ const findGlSparklineChart = () => wrapper.findComponent(GlSparklineChart);
beforeEach(() => {
- wrapper = shallowMount(Component, {
+ wrapper = shallowMount(MemoryGraph, {
propsData: {
metrics,
width: 100,
@@ -27,19 +23,15 @@ describe('MemoryGraph', () => {
});
});
- describe('chartData', () => {
- it('should calculate chartData', () => {
- expect(wrapper.vm.chartData.length).toEqual(metrics.length);
- });
-
- it('should format date & MB values', () => {
+ describe('Chart data', () => {
+ it('should have formatted date & MB values', () => {
const formattedData = [
['Nov 12 2019 19:17:33', '2.87'],
['Nov 12 2019 19:18:33', '2.78'],
['Nov 12 2019 19:19:33', '2.78'],
['Nov 12 2019 19:20:33', '3.01'],
];
- expect(wrapper.vm.chartData).toEqual(formattedData);
+ expect(findGlSparklineChart().props('data')).toEqual(formattedData);
});
});
@@ -47,7 +39,7 @@ describe('MemoryGraph', () => {
it('should draw container with chart', () => {
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.find('.memory-graph-container').exists()).toBe(true);
- expect(wrapper.findComponent(GlSparklineChart).exists()).toBe(true);
+ expect(findGlSparklineChart().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
index 537367940e0..626f6fc735e 100644
--- a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
@@ -4,11 +4,11 @@ import actionsFactory from '~/vue_shared/components/metric_images/store/actions'
import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
import createStore from '~/vue_shared/components/metric_images/store';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { fileList, initialData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const service = {
getMetricImages: jest.fn(),
uploadMetricImage: jest.fn(),
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index 61e4e774420..a649b06c50e 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -6,10 +6,6 @@ import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('modal copy button', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
wrapper = shallowMount(ModalCopyButton, {
propsData: {
diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
index b1bec28bffb..947ee756259 100644
--- a/spec/frontend/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -38,11 +38,6 @@ describe('navigation tabs component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render tabs', () => {
expect(wrapper.findAllComponents(GlTab)).toHaveLength(data.length);
});
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
index 31320b1d2a6..a116233a065 100644
--- a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -21,7 +21,7 @@ import {
searchProjectsWithinGroupQueryResponse,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('NewResourceDropdown component', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index 17a62ae8a33..f87674246d1 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -132,12 +132,6 @@ describe('Issue Warning Component', () => {
});
});
- afterEach(() => {
- wrapperLocked.destroy();
- wrapperConfidential.destroy();
- wrapperLockedAndConfidential.destroy();
- });
-
it('renders confidential & locked messages with noteable "issue"', () => {
expect(findLockedBlock(wrapperLocked).text()).toContain('This issue is locked.');
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 8f9f1bb336f..7e669fb7c71 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -30,11 +30,6 @@ describe('Issue placeholder note component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
index de6ab43bc41..5897b9e0ffc 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -12,11 +12,6 @@ describe('Placeholder system note component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index bcfd7a8ec70..29e1a9ccf4d 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -46,7 +46,6 @@ describe('system note component', () => {
});
afterEach(() => {
- vm.destroy();
mock.restore();
});
diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
index bd4b6a463ab..fa9d3cd28a9 100644
--- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
+++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -10,10 +10,6 @@ describe(`TimelineEntryItem`, () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders correctly', () => {
factory();
diff --git a/spec/frontend/vue_shared/components/ordered_layout_spec.js b/spec/frontend/vue_shared/components/ordered_layout_spec.js
index 21588569d6a..b6c8c467028 100644
--- a/spec/frontend/vue_shared/components/ordered_layout_spec.js
+++ b/spec/frontend/vue_shared/components/ordered_layout_spec.js
@@ -37,10 +37,6 @@ describe('Ordered Layout', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when slotKeys are in initial slot order', () => {
beforeEach(() => {
createComponent({ slotKeys: regularSlotOrder });
diff --git a/spec/frontend/vue_shared/components/page_size_selector_spec.js b/spec/frontend/vue_shared/components/page_size_selector_spec.js
index 5ec0b863afd..fce7ceee2fe 100644
--- a/spec/frontend/vue_shared/components/page_size_selector_spec.js
+++ b/spec/frontend/vue_shared/components/page_size_selector_spec.js
@@ -14,10 +14,6 @@ describe('Page size selector component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => {
createWrapper({ pageSize });
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index ae9c920ebd2..fc9adab2e2b 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -33,10 +33,6 @@ describe('Pagination links component', () => {
[glPaginatedList] = wrapper.vm.$children;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Paginated List Component', () => {
describe('props', () => {
// We test attrs and not props because we pass through to child component using v-bind:"$attrs"
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index 86a63db0d9e..25bfa688e5b 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -108,16 +108,23 @@ describe('AlertManagementEmptyState', () => {
const findStatusTabs = () => wrapper.findComponent(GlTabs);
const findStatusFilterBadge = () => wrapper.findAllComponents(GlBadge);
+ const handleFilterItems = (filters) => {
+ Filters().vm.$emit('onFilter', filters);
+ return nextTick();
+ };
+
describe('Snowplow tracking', () => {
+ const category = 'category';
+ const action = 'action';
+
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mountComponent({
- props: { trackViewsOptions: { category: 'category', action: 'action' } },
+ props: { trackViewsOptions: { category, action } },
});
});
it('should track the items list page views', () => {
- const { category, action } = wrapper.vm.trackViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
@@ -234,14 +241,14 @@ describe('AlertManagementEmptyState', () => {
findPagination().vm.$emit('input', 3);
await nextTick();
- expect(wrapper.vm.previousPage).toBe(2);
+ expect(findPagination().props('prevPage')).toBe(2);
});
it('returns 0 when it is the first page', async () => {
findPagination().vm.$emit('input', 1);
await nextTick();
- expect(wrapper.vm.previousPage).toBe(0);
+ expect(findPagination().props('prevPage')).toBe(0);
});
});
@@ -265,14 +272,14 @@ describe('AlertManagementEmptyState', () => {
findPagination().vm.$emit('input', 1);
await nextTick();
- expect(wrapper.vm.nextPage).toBe(2);
+ expect(findPagination().props('nextPage')).toBe(2);
});
it('returns `null` when currentPage is already last page', async () => {
findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1);
await nextTick();
- expect(wrapper.vm.nextPage).toBeNull();
+ expect(findPagination().props('nextPage')).toBeNull();
});
});
});
@@ -320,36 +327,32 @@ describe('AlertManagementEmptyState', () => {
it('returns correctly applied filter search values', async () => {
const searchTerm = 'foo';
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchTerm,
- });
-
+ await handleFilterItems([{ type: 'filtered-search-term', value: { data: searchTerm } }]);
await nextTick();
- expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]);
+ expect(Filters().props('initialFilterValue')).toEqual([searchTerm]);
});
- it('updates props tied to getIncidents GraphQL query', () => {
- wrapper.vm.handleFilterItems(mockFilters);
-
- expect(wrapper.vm.authorUsername).toBe('root');
- expect(wrapper.vm.assigneeUsername).toEqual('root2');
- expect(wrapper.vm.searchTerm).toBe(mockFilters[2].value.data);
- });
+ it('updates props tied to getIncidents GraphQL query', async () => {
+ await handleFilterItems(mockFilters);
- it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- authorUsername: 'foo',
- searchTerm: 'bar',
- });
+ const [
+ {
+ value: { data: authorUsername },
+ },
+ {
+ value: { data: assigneeUsername },
+ },
+ searchTerm,
+ ] = Filters().props('initialFilterValue');
- wrapper.vm.handleFilterItems([]);
+ expect(authorUsername).toBe('root');
+ expect(assigneeUsername).toEqual('root2');
+ expect(searchTerm).toBe(mockFilters[2].value.data);
+ });
- expect(wrapper.vm.authorUsername).toBe('');
- expect(wrapper.vm.searchTerm).toBe('');
+ it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', async () => {
+ await handleFilterItems([]);
+ expect(Filters().props('initialFilterValue')).toEqual([]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
index 112cdaf74c6..2a1a6342c38 100644
--- a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
@@ -25,10 +25,6 @@ describe('Pagination bar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('events', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js
index d444ad7a733..99a4f776305 100644
--- a/spec/frontend/vue_shared/components/pagination_links_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_links_spec.js
@@ -44,10 +44,6 @@ describe('Pagination links component', () => {
glPagination = wrapper.findComponent(GlPagination);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should provide translated text to GitLab UI pagination', () => {
Object.entries(translations).forEach((entry) => {
expect(glPagination.vm[entry[0]]).toBe(entry[1]);
diff --git a/spec/frontend/vue_shared/components/panel_resizer_spec.js b/spec/frontend/vue_shared/components/panel_resizer_spec.js
index 0e261124cbf..a535fe4939c 100644
--- a/spec/frontend/vue_shared/components/panel_resizer_spec.js
+++ b/spec/frontend/vue_shared/components/panel_resizer_spec.js
@@ -27,10 +27,6 @@ describe('Panel Resizer component', () => {
el.dispatchEvent(event);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a div element with the correct classes and styles', () => {
wrapper = mount(PanelResizer, {
propsData: {
diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
index ff4febd647e..a44a1aba8c0 100644
--- a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
+++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
@@ -16,10 +16,6 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', ()
const findAlert = () => wrapper.findComponent(GlAlert);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render alert with correct props', async () => {
createComponent({ errorMessages: [{ code: 'MissingQuotes' }] });
await nextTick();
diff --git a/spec/frontend/vue_shared/components/project_avatar_spec.js b/spec/frontend/vue_shared/components/project_avatar_spec.js
index af828fbca51..9378f6e3f1b 100644
--- a/spec/frontend/vue_shared/components/project_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/project_avatar_spec.js
@@ -15,10 +15,6 @@ describe('ProjectAvatar', () => {
wrapper = shallowMount(ProjectAvatar, { propsData: { ...defaultProps, ...props }, attrs });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders GlAvatar with correct props', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 4e0c318c84e..d704fcc0e7b 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,57 +1,49 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
describe('ProjectListItem component', () => {
- const Component = Vue.extend(ProjectListItem);
let wrapper;
- let vm;
- let options;
const project = JSON.parse(JSON.stringify(mockProjects))[0];
- beforeEach(() => {
- options = {
+ const createWrapper = ({ propsData } = {}) => {
+ wrapper = shallowMountExtended(ProjectListItem, {
propsData: {
project,
selected: false,
+ ...propsData,
},
- };
- });
-
- afterEach(() => {
- wrapper.vm.$destroy();
- });
-
- it('does not render a check mark icon if selected === false', () => {
- wrapper = shallowMount(Component, options);
-
- expect(wrapper.find('.js-selected-icon').exists()).toBe(false);
- });
+ });
+ };
- it('renders a check mark icon if selected === true', () => {
- options.propsData.selected = true;
+ const findProjectNamespace = () => wrapper.findByTestId('project-namespace');
+ const findProjectName = () => wrapper.findByTestId('project-name');
- wrapper = shallowMount(Component, options);
+ it.each([true, false])('renders a checkmark correctly when selected === "%s"', (selected) => {
+ createWrapper({
+ propsData: {
+ selected,
+ },
+ });
- expect(wrapper.find('.js-selected-icon').exists()).toBe(true);
+ expect(wrapper.findByTestId('selected-icon').exists()).toBe(selected);
});
- it(`emits a "clicked" event when clicked`, () => {
- wrapper = shallowMount(Component, options);
- ({ vm } = wrapper);
+ it(`emits a "clicked" event when the button is clicked`, () => {
+ createWrapper();
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- wrapper.vm.onClick();
+ expect(wrapper.emitted('click')).toBeUndefined();
+ wrapper.findComponent(GlButton).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ expect(wrapper.emitted('click')).toHaveLength(1);
});
it(`renders the project avatar`, () => {
- wrapper = shallowMount(Component, options);
+ createWrapper();
const avatar = wrapper.findComponent(ProjectAvatar);
expect(avatar.exists()).toBe(true);
@@ -63,48 +55,73 @@ describe('ProjectListItem component', () => {
});
it(`renders a simple namespace name with a trailing slash`, () => {
- options.propsData.project.name_with_namespace = 'a / b';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name_with_namespace: 'a / b',
+ },
+ },
+ });
+ const renderedNamespace = trimText(findProjectNamespace().text());
expect(renderedNamespace).toBe('a /');
});
it(`renders a properly truncated namespace with a trailing slash`, () => {
- options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name_with_namespace: 'a / b / c / d / e / f',
+ },
+ },
+ });
+ const renderedNamespace = trimText(findProjectNamespace().text());
expect(renderedNamespace).toBe('a / ... / e /');
});
it(`renders a simple namespace name of a GraphQL project`, () => {
- options.propsData.project.name_with_namespace = undefined;
- options.propsData.project.nameWithNamespace = 'test';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name_with_namespace: undefined,
+ nameWithNamespace: 'test',
+ },
+ },
+ });
+ const renderedNamespace = trimText(findProjectNamespace().text());
expect(renderedNamespace).toBe('test /');
});
it(`renders the project name`, () => {
- options.propsData.project.name = 'my-test-project';
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name: 'my-test-project',
+ },
+ },
+ });
+ const renderedName = trimText(findProjectName().text());
expect(renderedName).toBe('my-test-project');
});
it(`renders the project name with highlighting in the case of a search query match`, () => {
- options.propsData.project.name = 'my-test-project';
- options.propsData.matcher = 'pro';
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').html());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name: 'my-test-project',
+ },
+ matcher: 'pro',
+ },
+ });
+ const renderedName = trimText(findProjectName().html());
const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
expect(renderedName).toContain(expected);
@@ -112,11 +129,16 @@ describe('ProjectListItem component', () => {
it('prevents search query and project name XSS', () => {
const alertSpy = jest.spyOn(window, 'alert');
- options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
- options.propsData.matcher = "pro<script>alert('XSS');</script>";
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').html());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name: "my-xss-pro<script>alert('XSS');</script>ject",
+ },
+ matcher: "pro<script>alert('XSS');</script>",
+ },
+ });
+ const renderedName = trimText(findProjectName().html());
const expected = 'my-xss-project';
expect(renderedName).toContain(expected);
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index a0832dd7030..5e304f1c118 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -1,7 +1,7 @@
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { head } from 'lodash';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
@@ -25,7 +25,7 @@ describe('ProjectSelector component', () => {
};
beforeEach(() => {
- wrapper = mount(Vue.extend(ProjectSelector), {
+ wrapper = mount(ProjectSelector, {
propsData: {
projectSearchResults: searchResults,
selectedProjects: selected,
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 8f19f0ea14d..60c1293b7c1 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -26,10 +26,6 @@ describe('Package code instruction', () => {
const findInputElement = () => wrapper.find('[data-testid="instruction-input"]');
const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('single line', () => {
beforeEach(() =>
createComponent({
diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js
index ebc9816f983..9ef1ce5647d 100644
--- a/spec/frontend/vue_shared/components/registry/details_row_spec.js
+++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js
@@ -20,11 +20,6 @@ describe('DetailsRow', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('has a default slot', () => {
mountComponent();
expect(findDefaultSlot().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js
index 947520567e6..17abe06dbee 100644
--- a/spec/frontend/vue_shared/components/registry/history_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js
@@ -22,11 +22,6 @@ describe('History Item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index b941eb77c32..298fa163d59 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -30,11 +30,6 @@ describe('list item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
slotName | finderFunction
${'left-primary'} | ${findLeftPrimarySlot}
diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
index a04e1e237d4..278b09d80b2 100644
--- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
@@ -14,16 +14,11 @@ describe('Metadata Item', () => {
wrapper = shallowMount(component, {
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = (w = wrapper) => w.findComponent(GlLink);
const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
index 616fefe847e..b93fa37546f 100644
--- a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
+++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
@@ -31,10 +31,6 @@ describe('Persisted dropdown selection', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('local storage sync', () => {
it('uses the local storage sync component with the correct props', () => {
createComponent();
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 591447a37c2..59bb0646350 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -36,11 +36,6 @@ describe('Registry Search', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('searching', () => {
it('has a filtered-search component', () => {
mountComponent();
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index efb57ddd310..ec1451de470 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -36,11 +36,6 @@ describe('title area', () => {
return acc;
}, {});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('title', () => {
it('if slot is not present defaults to prop', () => {
mountComponent();
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
deleted file mode 100644
index cdfe311acd9..00000000000
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
+++ /dev/null
@@ -1,23 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Resizable Chart Container renders the component 1`] = `
-<div>
- <template>
- <div
- class="slot"
- >
- <span
- class="width"
- >
- 0
- </span>
-
- <span
- class="height"
- >
- 0
- </span>
- </div>
- </template>
-</div>
-`;
diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
deleted file mode 100644
index 7536df24ac6..00000000000
--- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { mount } from '@vue/test-utils';
-import $ from 'jquery';
-import { nextTick } from 'vue';
-import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
-
-jest.mock('~/lib/utils/common_utils', () => ({
- debounceByAnimationFrame(callback) {
- return jest.spyOn({ callback }, 'callback');
- },
-}));
-
-describe('Resizable Chart Container', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = mount(ResizableChartContainer, {
- scopedSlots: {
- default: `
- <template #default="{ width, height }">
- <div class="slot">
- <span class="width">{{width}}</span>
- <span class="height">{{height}}</span>
- </div>
- </template>
- `,
- },
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the component', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('updates the slot width and height props', async () => {
- const width = 1920;
- const height = 1080;
-
- // JSDOM mocks and sets clientWidth/clientHeight to 0 so we set manually
- wrapper.vm.$refs.chartWrapper = { clientWidth: width, clientHeight: height };
-
- $(document).trigger('content.resize');
-
- await nextTick();
- const widthNode = wrapper.find('.slot > .width');
- const heightNode = wrapper.find('.slot > .height');
-
- expect(parseInt(widthNode.text(), 10)).toEqual(width);
- expect(parseInt(heightNode.text(), 10)).toEqual(height);
- });
-
- it('calls onResize on manual resize', () => {
- $(document).trigger('content.resize');
- expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
- });
-
- it('calls onResize on page resize', () => {
- window.dispatchEvent(new Event('resize'));
- expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
index 5d96fe27676..11ee2e56c14 100644
--- a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
@@ -27,10 +27,6 @@ describe('RichTimestampTooltip', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the tooltip text header', () => {
expect(wrapper.findByTestId('header-text').text()).toBe('Created just now');
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
index f9d700fe67f..c4d4f80c573 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
@@ -59,10 +59,6 @@ describe('RunnerCliInstructions component', () => {
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockInstructions);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the instructions are shown', () => {
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 8f593b6aa1b..cb35cbd35ad 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -74,10 +74,6 @@ describe('RunnerInstructionsModal component', () => {
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockRunnerPlatforms);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the modal is shown', () => {
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
index 986d76d2b95..260eddbb37d 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -12,7 +12,7 @@ describe('RunnerInstructions component', () => {
const createComponent = () => {
wrapper = shallowMountExtended(RunnerInstructions, {
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-tooltip'),
},
});
};
@@ -21,10 +21,6 @@ describe('RunnerInstructions component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should show the "Show runner installation instructions" button', () => {
expect(findModalButton().text()).toBe('Show runner installation instructions');
});
diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
index 09b0b3d43ad..6eebd129beb 100644
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
@@ -6,7 +6,7 @@ import {
expectedDownloadDropdownPropsWithTitle,
securityReportMergeRequestDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
@@ -15,7 +15,7 @@ import {
} from '~/vue_shared/security_reports/constants';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Merge request artifact Download', () => {
let wrapper;
@@ -52,10 +52,6 @@ describe('Merge request artifact Download', () => {
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('given the query is loading', () => {
beforeEach(() => {
createWrapper({
diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
index 08d3d5b19d4..2f6e633fb34 100644
--- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
@@ -21,11 +21,6 @@ describe('HelpIcon component', () => {
const findPopover = () => wrapper.findComponent(GlPopover);
const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' });
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('given a help path only', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
index f186eb848f2..61cdc329220 100644
--- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
@@ -15,11 +15,6 @@ describe('SecuritySummary component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each([
{ message: '' },
{ message: 'foo' },
diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
index 88445b6684c..c1feb64dacb 100644
--- a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
+++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
@@ -40,10 +40,6 @@ describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
disabled,
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
index 5e829653c13..94d634f79bd 100644
--- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -16,10 +16,6 @@ describe('Settings Block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDefaultSlot = () => wrapper.findByTestId('default-slot');
const findTitleSlot = () => wrapper.findByTestId('title-slot');
const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
diff --git a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
index 8802a832781..e5d988f75f5 100644
--- a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
+++ b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
@@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => {
@@ -16,7 +15,7 @@ describe('Toggle Button', () => {
remain,
};
- const Component = Vue.extend({
+ const Component = {
components: {
SmartVirtualScrollList,
},
@@ -26,7 +25,7 @@ describe('Toggle Button', () => {
<smart-virtual-scroll-list v-bind="$options.smartListProperties">
<li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li>
</smart-virtual-scroll-list>`,
- });
+ };
return mount(Component).vm;
};
diff --git a/spec/frontend/vue_shared/components/source_editor_spec.js b/spec/frontend/vue_shared/components/source_editor_spec.js
index ca5b990bc29..5b155207029 100644
--- a/spec/frontend/vue_shared/components/source_editor_spec.js
+++ b/spec/frontend/vue_shared/components/source_editor_spec.js
@@ -47,10 +47,6 @@ describe('Source Editor component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const triggerChangeContent = (val) => {
mockInstance.getValue.mockReturnValue(val);
const [cb] = mockInstance.onDidChangeModelContent.mock.calls[0];
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
index da9067a8ddc..395ba92d4c6 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
@@ -45,8 +45,6 @@ describe('Chunk component', () => {
createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('Intersection observer', () => {
it('renders an Intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
index f661bd6747a..6c8fc244fa0 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -31,8 +31,6 @@ describe('Chunk Line component', () => {
createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('rendering', () => {
it('renders a blame link', () => {
expect(findBlameLink().attributes()).toMatchObject({
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index 95ef11d776a..59880496d74 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -24,8 +24,6 @@ describe('Chunk component', () => {
createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('Intersection observer', () => {
it('renders an Intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
index 0beec8e9d3e..c911e3d308b 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
@@ -68,8 +68,6 @@ describe('Source Viewer component', () => {
return createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: language };
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 1c75442b4a8..46b582c3668 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
@@ -25,8 +25,6 @@ describe('Source Viewer component', () => {
return createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
index 79b1f17afa0..13911d487f2 100644
--- a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -18,10 +18,6 @@ describe('StackedProgressBarComponent', () => {
wrapper = mount(StackedProgressBarComponent, { propsData: defaultConfig });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSuccessBar = () => wrapper.find('.status-green');
const findNeutralBar = () => wrapper.find('.status-neutral');
const findFailureBar = () => wrapper.find('.status-red');
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index 99de26ce2ae..79aba1b2516 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -16,10 +16,6 @@ describe('Pagination component', () => {
spy = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('render', () => {
it('should not render anything', () => {
mountComponent({
diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
index 28c5acc8110..a1757952dc0 100644
--- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
@@ -25,7 +25,6 @@ describe('Time ago with tooltip component', () => {
};
afterEach(() => {
- vm.destroy();
timezoneMock.unregister();
});
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
index c8351ed61d7..d8dedd8240b 100644
--- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
+import { GlDropdownItem, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
@@ -9,7 +9,9 @@ describe('Deploy freeze timezone dropdown', () => {
let wrapper;
let store;
- const createComponent = (searchTerm, selectedTimezone) => {
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const createComponent = async (searchTerm, selectedTimezone) => {
wrapper = shallowMountExtended(TimezoneDropdown, {
store,
propsData: {
@@ -19,9 +21,8 @@ describe('Deploy freeze timezone dropdown', () => {
},
});
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchTerm });
+ findSearchBox().vm.$emit('input', searchTerm);
+ await nextTick();
};
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
@@ -29,14 +30,9 @@ describe('Deploy freeze timezone dropdown', () => {
const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults');
const findHiddenInput = () => wrapper.find('input');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('No time zones found', () => {
- beforeEach(() => {
- createComponent('UTC timezone');
+ beforeEach(async () => {
+ await createComponent('UTC timezone');
});
it('renders empty results message', () => {
@@ -45,8 +41,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
+ beforeEach(async () => {
+ await createComponent('');
});
it('renders all timezones when search term is empty', () => {
@@ -55,8 +51,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Time zones found', () => {
- beforeEach(() => {
- createComponent('Alaska');
+ beforeEach(async () => {
+ await createComponent('Alaska');
});
it('renders only the time zone searched for', () => {
@@ -87,8 +83,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Selected time zone not found', () => {
- beforeEach(() => {
- createComponent('', 'Berlin');
+ beforeEach(async () => {
+ await createComponent('', 'Berlin');
});
it('renders empty selections', () => {
@@ -101,8 +97,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Selected time zone found', () => {
- beforeEach(() => {
- createComponent('', 'Europe/Berlin');
+ beforeEach(async () => {
+ await createComponent('', 'Europe/Berlin');
});
it('renders selected time zone as dropdown label', () => {
diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
index ca1f7996ad6..3807bb4cc63 100644
--- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
+++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
@@ -30,8 +30,8 @@ describe('TooltipOnTruncate component', () => {
default: [MOCK_TITLE],
},
directives: {
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
...options,
});
@@ -42,8 +42,8 @@ describe('TooltipOnTruncate component', () => {
...TooltipOnTruncate,
directives: {
...TooltipOnTruncate.directives,
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
};
@@ -78,10 +78,6 @@ describe('TooltipOnTruncate component', () => {
await nextTick();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when truncated', () => {
beforeEach(async () => {
hasHorizontalOverflow.mockReturnValueOnce(true);
@@ -90,7 +86,7 @@ describe('TooltipOnTruncate component', () => {
it('renders tooltip', async () => {
expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element);
- expect(getTooltipValue()).toMatchObject({
+ expect(getTooltipValue()).toStrictEqual({
title: MOCK_TITLE,
placement: 'top',
disabled: false,
@@ -144,7 +140,7 @@ describe('TooltipOnTruncate component', () => {
await nextTick();
- expect(getTooltipValue()).toMatchObject({
+ expect(getTooltipValue()).toStrictEqual({
title: MOCK_TITLE,
placement: 'top',
disabled: false,
@@ -194,20 +190,22 @@ describe('TooltipOnTruncate component', () => {
});
});
- describe('placement', () => {
- it('sets placement when tooltip is rendered', () => {
- const mockPlacement = 'bottom';
-
+ describe('tooltip customization', () => {
+ it.each`
+ property | mockValue
+ ${'placement'} | ${'bottom'}
+ ${'boundary'} | ${'viewport'}
+ `('sets $property when the tooltip is rendered', ({ property, mockValue }) => {
hasHorizontalOverflow.mockReturnValueOnce(true);
createComponent({
propsData: {
- placement: mockPlacement,
+ [property]: mockValue,
},
});
expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element);
expect(getTooltipValue()).toMatchObject({
- placement: mockPlacement,
+ [property]: mockValue,
});
});
});
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 f9d615d4f68..c816fe790a8 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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-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 gl-mb-0"
+ class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
type="button"
>
<div
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index a063a5591e3..24f96195e05 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -3,8 +3,6 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-jest.mock('~/flash');
-
describe('Upload dropzone component', () => {
let wrapper;
@@ -34,11 +32,6 @@ describe('Upload dropzone component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when slot provided', () => {
it('renders dropzone with slot content', () => {
createComponent({
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index 30a7439579f..2718be74111 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -33,10 +33,6 @@ describe('url sync component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const expectUrlSyncWithMergeUrlParams = (
query,
times,
diff --git a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
index 662c09d02bf..ba55df5512f 100644
--- a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
+++ b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
@@ -24,10 +24,6 @@ describe('usage banner', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
slotName | finderFunction
${'left-primary-text'} | ${findLeftPrimaryTextSlot}
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
index d63b13981ac..3ae3d89af27 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -20,10 +20,6 @@ describe('User Avatar Image Component', () => {
const findAvatar = () => wrapper.findComponent(GlAvatar);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Initialization', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index df7ce449678..90f9156af38 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -34,10 +34,6 @@ describe('User Avatar Link Component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render GlLink with correct props', () => {
const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 63371b1492b..1754292cb63 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -50,10 +50,6 @@ describe('UserAvatarList', () => {
props = { imgSize: TEST_IMAGE_SIZE };
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('empty text', () => {
it('shows when items are empty', () => {
factory({ propsData: { items: [] } });
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 521744154ba..b04e578c931 100644
--- a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
+++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
@@ -28,8 +28,6 @@ const initialSlotProps = (changes = {}) => ({
});
describe('UserCalloutDismisser', () => {
- let wrapper;
-
const MOCK_FEATURE_NAME = 'mock_feature_name';
// Query handlers
@@ -52,7 +50,7 @@ describe('UserCalloutDismisser', () => {
const callDismissSlotProp = () => defaultScopedSlotSpy.mock.calls[0][0].dismiss();
const createComponent = ({ queryHandler, mutationHandler, ...options }) => {
- wrapper = mount(
+ mount(
UserCalloutDismisser,
merge(
{
@@ -72,10 +70,6 @@ describe('UserCalloutDismisser', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
index 78abb89e7b8..6491e5a66cd 100644
--- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -51,10 +51,6 @@ describe('User deletion obstacles list', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findLinks = () => wrapper.findAllComponents(GlLink);
const findTitle = () => wrapper.findByTestId('title');
const findFooter = () => wrapper.findByTestId('footer');
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 f6316af6ad8..8ecab5cc043 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
@@ -13,11 +13,11 @@ import {
I18N_ERROR_UNFOLLOW,
} from '~/vue_shared/components/user_popover/constants';
import axios from '~/lib/utils/axios_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { followUser, unfollowUser } from '~/api/user_api';
import { mockTracking } from 'helpers/tracking_helper';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/api/user_api', () => ({
followUser: jest.fn(),
unfollowUser: jest.fn(),
@@ -51,7 +51,6 @@ describe('User Popover Component', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index b0e9584a15b..e881bfed35e 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import getIssueParticipantsQuery from '~/sidebar/queries/get_issue_participants.query.graphql';
@@ -105,7 +105,6 @@ describe('User select dropdown', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -409,7 +408,7 @@ describe('User select dropdown', () => {
describe('when on merge request sidebar', () => {
beforeEach(() => {
- createComponent({ props: { issuableType: IssuableType.MergeRequest, issuableId: 1 } });
+ createComponent({ props: { issuableType: TYPE_MERGE_REQUEST, issuableId: 1 } });
return waitForPromises();
});
diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
index c136c2054ac..acbb931b7b6 100644
--- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
+++ b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
@@ -27,10 +27,6 @@ describe('~/vue_shared/components/vuex_module_provider', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('provides "vuexModule" set from prop', () => {
createComponent();
expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 18afe049149..f6eb11aaddf 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -137,10 +137,6 @@ describe('Web IDE link component', () => {
localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, 'true');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findActionsButton = () => wrapper.findComponent(ActionsButton);
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js
index dcd3a44a6fc..72a348c1a79 100644
--- a/spec/frontend/vue_shared/directives/validation_spec.js
+++ b/spec/frontend/vue_shared/directives/validation_spec.js
@@ -80,11 +80,6 @@ describe('validation directive', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const getFormData = () => wrapper.vm.form;
const findForm = () => wrapper.find('form');
const findInput = () => wrapper.find('input');
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
index 7b0f0f7e344..e983519d9fc 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
@@ -34,10 +34,6 @@ describe('IssuableCreateRoot', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders component container element with class "issuable-create-container"', () => {
expect(wrapper.classes()).toContain('issuable-create-container');
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index ff21b3bc356..ae2fd5ebffa 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -36,10 +36,6 @@ describe('IssuableForm', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('handleUpdateSelectedLabels', () => {
it('sets provided `labels` param to prop `selectedLabels`', () => {
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
index 76b6efa15b6..ce9e23d9a00 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
@@ -6,11 +6,8 @@ import {
} from 'jest/sidebar/components/labels/labels_select_widget/mock_data';
import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
-import {
- DropdownVariant,
- LabelType,
-} from '~/sidebar/components/labels/labels_select_widget/constants';
-import { WorkspaceType } from '~/issues/constants';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import { WORKSPACE_PROJECT } from '~/issues/constants';
import { __ } from '~/locale';
const allowLabelRemove = true;
@@ -20,9 +17,9 @@ const fullPath = '/full-path';
const labelsFilterBasePath = '/labels-filter-base-path';
const initialLabels = [];
const issuableType = 'issue';
-const labelType = LabelType.project;
+const labelType = WORKSPACE_PROJECT;
const variant = DropdownVariant.Embedded;
-const workspaceType = WorkspaceType.project;
+const workspaceType = WORKSPACE_PROJECT;
describe('IssuableLabelSelector', () => {
let wrapper;
@@ -50,10 +47,6 @@ describe('IssuableLabelSelector', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const expectTitleWithCount = (count) => {
const title = findTitle();
diff --git a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
index a0b1d64b97c..61e6d2a420a 100644
--- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -7,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants';
-import { issuableTypes } from '~/boards/constants';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { truncate } from '~/lib/utils/text_utility';
import {
mockIssue,
@@ -49,11 +48,6 @@ describe('IssuableBlockedIcon', () => {
await waitForApollo();
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createWrapperWithApollo = ({
item = mockBlockedIssue1,
blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
@@ -121,9 +115,9 @@ describe('IssuableBlockedIcon', () => {
};
it.each`
- mockIssuable | issuableType | expectedIcon
- ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'}
- ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
+ mockIssuable | issuableType | expectedIcon
+ ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'}
+ ${mockEpic} | ${TYPE_EPIC} | ${'entity-blocked'}
`(
'should render blocked icon for $issuableType',
({ mockIssuable, issuableType, expectedIcon }) => {
@@ -153,9 +147,9 @@ describe('IssuableBlockedIcon', () => {
describe('on mouseenter on blocked icon', () => {
it.each`
- item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
- ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
- ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
+ item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
+ ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
+ ${mockBlockedEpic1} | ${TYPE_EPIC} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
`(
'should query for blocking issuables and render the result for $issuableType',
async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => {
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
index a25f92c9cf2..c23bd002ee5 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
@@ -28,7 +28,6 @@ describe('IssuableBulkEditSidebar', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 2fac004875a..45daf0dc34b 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -39,7 +39,6 @@ describe('IssuableItem', () => {
const mockLabels = mockIssuable.labels.nodes;
const mockAuthor = mockIssuable.author;
- const originalUrl = gon.gitlab_url;
let wrapper;
const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]');
@@ -49,11 +48,6 @@ describe('IssuableItem', () => {
gon.gitlab_url = MOCK_GITLAB_URL;
});
- afterEach(() => {
- wrapper.destroy();
- gon.gitlab_url = originalUrl;
- });
-
describe('computed', () => {
describe('author', () => {
it('returns `issuable.author` reference', () => {
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 371844e66f4..9a4636e0f4d 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -47,10 +47,6 @@ describe('IssuableListRoot', () => {
const findVueDraggable = () => wrapper.findComponent(VueDraggable);
const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
index 27985895c62..9cdd4d75c42 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
@@ -35,7 +35,6 @@ describe('IssuableTabs', () => {
afterEach(() => {
setLanguage(null);
- wrapper.destroy();
});
const findAllGlBadges = () => wrapper.findAllComponents(GlBadge);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 6b20f0c77a3..7e665b7c76e 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -13,7 +13,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
jest.mock('~/autosave');
-jest.mock('~/flash');
+jest.mock('~/alert');
const issuableBodyProps = {
...mockIssuableShowProps,
@@ -48,10 +48,6 @@ describe('IssuableBody', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('isUpdated', () => {
it.each`
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
index ea58cc2baf5..b4f1c286158 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
@@ -24,10 +24,6 @@ describe('IssuableDescription', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mounted', () => {
it('calls `renderGFM`', () => {
expect(renderGFM).toHaveBeenCalledTimes(1);
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 159be4cd1ef..0d6cd1ad00b 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
@@ -43,6 +43,9 @@ describe('IssuableEditForm', () => {
});
afterEach(() => {
+ // note: the order of wrapper.destroy() and jest.resetAllMocks() matters.
+ // maybe it'll help with investigation on how to remove this wrapper.destroy() call
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
jest.resetAllMocks();
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 6a8b9ef77a9..d9f1b6c15a8 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -33,7 +33,6 @@ describe('IssuableHeader', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index edfd55c8bb4..f976e0499f0 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -41,10 +41,6 @@ describe('IssuableShowRoot', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
const {
statusIcon,
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index 6f62fb77353..39316dfa249 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -22,35 +22,35 @@ const createComponent = (propsData = issuableTitleProps) =>
'status-badge': 'Open',
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
describe('IssuableTitle', () => {
let wrapper;
+ const findStickyHeader = () => wrapper.findComponent('[data-testid="header"]');
+
beforeEach(() => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('handleTitleAppear', () => {
- it('sets value of `stickyTitleVisible` prop to false', () => {
+ it('sets value of `stickyTitleVisible` prop to false', async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+ await nextTick();
- expect(wrapper.vm.stickyTitleVisible).toBe(false);
+ expect(findStickyHeader().exists()).toBe(false);
});
});
describe('handleTitleDisappear', () => {
- it('sets value of `stickyTitleVisible` prop to true', () => {
+ it('sets value of `stickyTitleVisible` prop to true', async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
- expect(wrapper.vm.stickyTitleVisible).toBe(true);
+ expect(findStickyHeader().exists()).toBe(true);
});
});
});
@@ -87,14 +87,10 @@ describe('IssuableTitle', () => {
});
it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- stickyTitleVisible: true,
- });
-
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
await nextTick();
- const stickyHeaderEl = wrapper.find('[data-testid="header"]');
+
+ const stickyHeaderEl = findStickyHeader();
expect(stickyHeaderEl.exists()).toBe(true);
expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
index 6c9e5f85fa0..f2509aead77 100644
--- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
@@ -38,7 +38,6 @@ describe('IssuableSidebarRoot', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js
index 52f36aa0e77..052ff518468 100644
--- a/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js
@@ -11,9 +11,7 @@ describe('Legacy container component', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
- wrapper = null;
});
describe('when selector targets real node', () => {
diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
index c90131fea9a..cc8a8d86d19 100644
--- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
@@ -27,9 +27,7 @@ describe('Welcome page', () => {
});
afterEach(() => {
- wrapper.destroy();
window.location.hash = '';
- wrapper = null;
});
it('tracks link clicks', async () => {
diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
index 6115dc6e61b..5ff7b9f390a 100644
--- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
@@ -14,7 +14,7 @@ describe('Experimental new project creation app', () => {
const DEFAULT_PROPS = {
title: 'Create something',
- initialBreadcrumb: 'Something',
+ initialBreadcrumbs: [{ text: 'Something', href: '#' }],
panels: [
{ name: 'panel1', selector: '#some-selector1' },
{ name: 'panel2', selector: '#some-selector2' },
@@ -33,7 +33,6 @@ describe('Experimental new project creation app', () => {
};
afterEach(() => {
- wrapper.destroy();
window.location.hash = '';
});
@@ -46,8 +45,8 @@ describe('Experimental new project creation app', () => {
expect(findWelcomePage().exists()).toBe(true);
});
- it('does not render breadcrumbs', () => {
- expect(findBreadcrumb().exists()).toBe(false);
+ it('renders breadcrumbs', () => {
+ expect(findBreadcrumb().exists()).toBe(true);
});
});
@@ -75,7 +74,7 @@ describe('Experimental new project creation app', () => {
it('renders breadcrumbs', () => {
const breadcrumb = findBreadcrumb();
expect(breadcrumb.exists()).toBe(true);
- expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumb);
+ expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumbs[0].text);
});
});
diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js
index 322586a772c..0bf2737fb2b 100644
--- a/spec/frontend/vue_shared/plugins/global_toast_spec.js
+++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js
@@ -1,14 +1,16 @@
-import toast, { instance } from '~/vue_shared/plugins/global_toast';
+import toast from '~/vue_shared/plugins/global_toast';
-describe('Global toast', () => {
- let spyFunc;
-
- beforeEach(() => {
- spyFunc = jest.spyOn(instance.$toast, 'show').mockImplementation(() => {});
- });
+const mockSpy = jest.fn();
+jest.mock('@gitlab/ui', () => ({
+ GlToast: (Vue) => {
+ // eslint-disable-next-line no-param-reassign
+ Vue.prototype.$toast = { show: (...args) => mockSpy(...args) };
+ },
+}));
+describe('Global toast', () => {
afterEach(() => {
- spyFunc.mockRestore();
+ mockSpy.mockRestore();
});
it("should call GitLab UI's toast method", () => {
@@ -17,7 +19,7 @@ describe('Global toast', () => {
toast(arg1, arg2);
- expect(instance.$toast.show).toHaveBeenCalledTimes(1);
- expect(instance.$toast.show).toHaveBeenCalledWith(arg1, arg2);
+ expect(mockSpy).toHaveBeenCalledTimes(1);
+ expect(mockSpy).toHaveBeenCalledWith(arg1, arg2);
});
});
diff --git a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
index 136fe74b0d6..d258658d5e2 100644
--- a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
+++ b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
@@ -21,10 +21,6 @@ describe('Section Layout component', () => {
const findHeading = () => wrapper.find('h2');
const findLoader = () => wrapper.findComponent(SectionLoader);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('basic structure', () => {
beforeEach(() => {
createComponent({ heading: 'testheading' });
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 0a5e46d9263..6345393951c 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
@@ -56,10 +56,6 @@ describe('ManageViaMr component', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
// This component supports different report types/mutations depending on
// whether it's in a CE or EE context. This makes sure we are only testing
// the ones available in the current test context.
diff --git a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
index 5f2b13a79c9..299a3d62421 100644
--- a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
@@ -15,11 +15,6 @@ describe('SecurityReportDownloadDropdown component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('given report artifacts', () => {
beforeEach(() => {
artifacts = [
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 221da35de3d..257f59612e8 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -14,7 +14,7 @@ import {
sastDiffSuccessMock,
secretDetectionDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
@@ -26,7 +26,7 @@ import {
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
Vue.use(Vuex);
@@ -74,10 +74,6 @@ describe('Security reports app', () => {
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
const findHelpIconComponent = () => wrapper.findComponent(HelpIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('given the artifacts query is loading', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
index 45a39d2dd58..cbeff184e9d 100644
--- a/spec/frontend/webhooks/components/form_url_app_spec.js
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -19,10 +19,6 @@ describe('FormUrlApp', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAllRadioButtons = () => wrapper.findAllComponents(GlFormRadio);
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findUrlMaskDisable = () => findAllRadioButtons().at(0);
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index ee15034daff..000b07f4dfd 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -49,7 +49,7 @@ describe('App', () => {
store,
propsData: buildProps(),
directives: {
- GlResizeObserver: createMockDirective(),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
};
@@ -71,7 +71,6 @@ describe('App', () => {
};
afterEach(() => {
- wrapper.destroy();
unmockTracking();
});
diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js
index 099054bf8ca..d69ac2803df 100644
--- a/spec/frontend/whats_new/components/feature_spec.js
+++ b/spec/frontend/whats_new/components/feature_spec.js
@@ -30,11 +30,6 @@ describe("What's new single feature", () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders the date', () => {
createWrapper({ feature: exampleFeature });
diff --git a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
index b199f4f0c49..79717b8767e 100644
--- a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
+++ b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
@@ -11,10 +11,6 @@ describe('~/whats_new/utils/get_drawer_body_height', () => {
});
});
- afterEach(() => {
- drawerWrapper.destroy();
- });
-
const setClientHeight = (el, height) => {
Object.defineProperty(el, 'clientHeight', {
get() {
diff --git a/spec/frontend/work_items/components/app_spec.js b/spec/frontend/work_items/components/app_spec.js
index 95034085493..d799e8042b1 100644
--- a/spec/frontend/work_items/components/app_spec.js
+++ b/spec/frontend/work_items/components/app_spec.js
@@ -12,10 +12,6 @@ describe('Work Items Application', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a component', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js
index c3cc2fbc556..c3bdbfe030e 100644
--- a/spec/frontend/work_items/components/item_state_spec.js
+++ b/spec/frontend/work_items/components/item_state_spec.js
@@ -21,10 +21,6 @@ describe('ItemState', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders label and dropdown', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 6361f8dafc4..aef310319ab 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -19,10 +19,6 @@ describe('ItemTitle', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title contents', () => {
expect(findInputEl().attributes()).toMatchObject({
'data-placeholder': 'Add a title...',
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
index 5901642b8a1..1c01451f047 100644
--- a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\"></note-header-stub>"`;
+exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\" noteurl=\\"\\"></note-header-stub>"`;
diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js
deleted file mode 100644
index eb4bcbf942b..00000000000
--- a/spec/frontend/work_items/components/notes/activity_filter_spec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { ASC, DESC } from '~/notes/constants';
-
-import { mockTracking } from 'helpers/tracking_helper';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-
-describe('Activity Filter', () => {
- let wrapper;
-
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first');
-
- const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => {
- wrapper = shallowMountExtended(ActivityFilter, {
- propsData: {
- sortOrder,
- loading,
- workItemType,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- describe('default', () => {
- it('has a dropdown with 2 options', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length);
- });
-
- it('has local storage sync with the correct props', () => {
- expect(findLocalStorageSync().props('asString')).toBe(true);
- });
-
- it('emits `updateSavedSortOrder` event when update is emitted', async () => {
- findLocalStorageSync().vm.$emit('input', ASC);
-
- await nextTick();
- expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1);
- expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]);
- });
- });
-
- describe('when asc', () => {
- describe('when the dropdown is clicked', () => {
- it('calls the right actions', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- findNewestFirstItem().vm.$emit('click');
- await nextTick();
-
- expect(wrapper.emitted('changeSortOrder')).toHaveLength(1);
- expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]);
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'notes_sort_order_changed',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_sorting',
- property: 'type_Task',
- },
- );
- });
- });
- });
-});
diff --git a/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
new file mode 100644
index 00000000000..5ed9d581446
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
@@ -0,0 +1,109 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemActivitySortFilter from '~/work_items/components/notes/work_item_activity_sort_filter.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { ASC, DESC } from '~/notes/constants';
+import {
+ WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ WORK_ITEM_NOTES_FILTER_KEY,
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
+ TRACKING_CATEGORY_SHOW,
+} from '~/work_items/constants';
+
+import { mockTracking } from 'helpers/tracking_helper';
+
+describe('Work Item Activity/Discussions Filtering', () => {
+ let wrapper;
+
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findByDataTestId = (dataTestId) => wrapper.findByTestId(dataTestId);
+
+ const createComponent = ({
+ loading = false,
+ workItemType = 'Task',
+ sortFilterProp = ASC,
+ filterOptions = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ trackingLabel = 'item_track_notes_sorting',
+ trackingAction = 'work_item_notes_sort_order_changed',
+ filterEvent = 'changeSort',
+ defaultSortFilterProp = ASC,
+ storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemActivitySortFilter, {
+ propsData: {
+ loading,
+ workItemType,
+ sortFilterProp,
+ filterOptions,
+ trackingLabel,
+ trackingAction,
+ filterEvent,
+ defaultSortFilterProp,
+ storageKey,
+ },
+ });
+ };
+
+ describe.each`
+ usedFor | filterOptions | storageKey | filterEvent | newInputOption | trackingLabel | trackingAction | defaultSortFilterProp | sortFilterProp | nonDefaultDataTestId
+ ${'Sorting'} | ${WORK_ITEM_ACTIVITY_SORT_OPTIONS} | ${WORK_ITEM_NOTES_SORT_ORDER_KEY} | ${'changeSort'} | ${DESC} | ${'item_track_notes_sorting'} | ${'work_item_notes_sort_order_changed'} | ${ASC} | ${ASC} | ${'newest-first'}
+ ${'Filtering'} | ${WORK_ITEM_ACTIVITY_FILTER_OPTIONS} | ${WORK_ITEM_NOTES_FILTER_KEY} | ${'changeFilter'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${'item_track_notes_sorting'} | ${'work_item_notes_filter_changed'} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${'comments-activity'}
+ `(
+ 'When used for $usedFor',
+ ({
+ filterOptions,
+ storageKey,
+ filterEvent,
+ trackingLabel,
+ trackingAction,
+ newInputOption,
+ defaultSortFilterProp,
+ sortFilterProp,
+ nonDefaultDataTestId,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ sortFilterProp,
+ filterOptions,
+ trackingLabel,
+ trackingAction,
+ filterEvent,
+ defaultSortFilterProp,
+ storageKey,
+ });
+ });
+
+ it('has a dropdown with options equal to the length of `filterOptions`', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(filterOptions.length);
+ });
+
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
+ expect(findLocalStorageSync().props('storageKey')).toBe(storageKey);
+ });
+
+ it(`emits ${filterEvent} event when local storage input is emitted`, () => {
+ findLocalStorageSync().vm.$emit('input', newInputOption);
+
+ expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]);
+ });
+
+ it('emits tracking event when the a non default dropdown item is clicked', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ findByDataTestId(nonDefaultDataTestId).vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, trackingAction, {
+ category: TRACKING_CATEGORY_SHOW,
+ label: trackingLabel,
+ property: 'type_Task',
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index bb65b75c4d8..6b95da0910b 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -1,7 +1,5 @@
-import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
@@ -21,9 +19,6 @@ describe('Work Item Discussion', () => {
let wrapper;
const mockWorkItemId = 'gid://gitlab/WorkItem/625';
- const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
- const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
- const findAvatar = () => wrapper.findComponent(GlAvatar);
const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
const findAllThreads = () => wrapper.findAllComponents(WorkItemNote);
const findThreadAtIndex = (index) => findAllThreads().at(index);
@@ -55,19 +50,6 @@ describe('Work Item Discussion', () => {
createComponent();
});
- it('Should be wrapped inside the timeline entry item', () => {
- expect(findTimelineEntryItem().exists()).toBe(true);
- });
-
- it('should have the author avatar of the work item note', () => {
- expect(findAvatarLink().exists()).toBe(true);
- expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
-
- expect(findAvatar().exists()).toBe(true);
- expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
- expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
- });
-
it('should not show the the toggle replies widget wrapper when no replies', () => {
expect(findToggleRepliesWidget().exists()).toBe(false);
});
@@ -89,13 +71,17 @@ describe('Work Item Discussion', () => {
});
it('the number of threads should be equal to the response length', async () => {
- findToggleRepliesWidget().vm.$emit('toggle');
- await nextTick();
expect(findAllThreads()).toHaveLength(
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length,
);
});
+ it('should collapse when we click on toggle replies widget', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAllThreads()).toHaveLength(1);
+ });
+
it('should autofocus when we click expand replies', async () => {
const mainComment = findThreadAtIndex(0);
diff --git a/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js
new file mode 100644
index 00000000000..339efad0608
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js
@@ -0,0 +1,44 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+} from '~/work_items/constants';
+
+describe('Work Item History Filter note', () => {
+ let wrapper;
+
+ const findShowAllActivityButton = () => wrapper.findByTestId('show-all-activity');
+ const findShowCommentsButton = () => wrapper.findByTestId('show-comments-only');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(WorkItemHistoryOnlyFilterNote, {
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('timelineContent renders a string containing instruction for switching feed type', () => {
+ expect(wrapper.text()).toContain(
+ "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+
+ it('emits `changeFilter` event with 0 parameter on clicking Show all activity button', () => {
+ findShowAllActivityButton().vm.$emit('click');
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ALL_NOTES]]);
+ });
+
+ it('emits `changeFilter` event with 1 parameter on clicking Show comments only button', () => {
+ findShowCommentsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index d85cd46c1c3..b293127b6af 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,52 +1,116 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import EmojiPicker from '~/emoji/components/picker.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+
+Vue.use(VueApollo);
describe('Work Item Note Actions', () => {
let wrapper;
+ const noteId = '1';
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
+ const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
+
+ const addEmojiMutationResolver = jest.fn().mockResolvedValue({
+ data: {
+ errors: [],
+ },
+ });
+
+ const EmojiPickerStub = {
+ props: EmojiPicker.props,
+ template: '<div></div>',
+ };
- const createComponent = ({ showReply = true, showEdit = true } = {}) => {
+ const createComponent = ({ showReply = true, showEdit = true, showAwardEmoji = true } = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
+ noteId,
+ showAwardEmoji,
+ },
+ provide: {
+ glFeatures: {
+ workItemsMvc2: true,
+ },
},
+ stubs: {
+ EmojiPicker: EmojiPickerStub,
+ },
+ apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]),
});
};
- describe('Default', () => {
- it('Should show the reply button by default', () => {
+ describe('reply button', () => {
+ it('is visible by default', () => {
createComponent();
+
expect(findReplyButton().exists()).toBe(true);
});
- });
- describe('When the reply button needs to be hidden', () => {
- it('Should show the reply button by default', () => {
+ it('is hidden when showReply false', () => {
createComponent({ showReply: false });
+
expect(findReplyButton().exists()).toBe(false);
});
});
- it('shows edit button when `showEdit` prop is true', () => {
- createComponent();
+ describe('edit button', () => {
+ it('is visible when `showEdit` prop is true', () => {
+ createComponent();
- expect(findEditButton().exists()).toBe(true);
- });
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('is hidden when `showEdit` prop is false', () => {
+ createComponent({ showEdit: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
- it('does not show edit button when `showEdit` prop is false', () => {
- createComponent({ showEdit: false });
+ it('emits `startEditing` event when clicked', () => {
+ createComponent();
+ findEditButton().vm.$emit('click');
- expect(findEditButton().exists()).toBe(false);
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
});
- it('emits `startEditing` event when edit button is clicked', () => {
- createComponent();
- findEditButton().vm.$emit('click');
+ describe('emoji picker', () => {
+ it('is visible when `showAwardEmoji` prop is true', () => {
+ createComponent();
+
+ expect(findEmojiButton().exists()).toBe(true);
+ });
+
+ it('is hidden when `showAwardEmoji` prop is false', () => {
+ createComponent({ showAwardEmoji: false });
- expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ expect(findEmojiButton().exists()).toBe(false);
+ });
+
+ it('commits mutation on click', async () => {
+ const awardName = 'carrot';
+
+ createComponent();
+
+ findEmojiButton().vm.$emit('click', awardName);
+
+ await waitForPromises();
+
+ expect(findEmojiButton().emitted('errors')).toEqual(undefined);
+ expect(addEmojiMutationResolver).toHaveBeenCalledWith({
+ awardableId: noteId,
+ name: awardName,
+ });
+ });
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 9b87419cee7..8e574dc1a81 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -198,10 +198,6 @@ describe('Work Item Note', () => {
expect(findNoteActions().exists()).toBe(true);
});
- it('should not have the Avatar link for main thread inside the timeline-entry', () => {
- expect(findAuthorAvatarLink().exists()).toBe(false);
- });
-
it('should have the reply button props', () => {
expect(findNoteActions().props('showReply')).toBe(true);
});
@@ -228,7 +224,7 @@ describe('Work Item Note', () => {
});
});
- it('should display a dropdown if user has a permission to delete a note', () => {
+ it('should display the `Delete comment` dropdown item if user has a permission to delete a note', () => {
createComponent({
note: {
...mockWorkItemCommentNote,
@@ -237,12 +233,14 @@ describe('Work Item Note', () => {
});
expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(true);
});
- it('should not display a dropdown if user has no permission to delete a note', () => {
+ it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => {
createComponent();
- expect(findDropdown().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(false);
});
it('should emit `deleteNote` event when delete note action is clicked', () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
new file mode 100644
index 00000000000..daf74f7a93b
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
@@ -0,0 +1,63 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
+import { ASC } from '~/notes/constants';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+} from '~/work_items/constants';
+
+describe('Work Item Note Activity Header', () => {
+ let wrapper;
+
+ const findActivityLabelHeading = () => wrapper.find('h3');
+ const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter');
+ const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort');
+
+ const createComponent = ({
+ disableActivityFilterSort = false,
+ sortOrder = ASC,
+ workItemType = 'Task',
+ discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemNotesActivityHeader, {
+ propsData: {
+ disableActivityFilterSort,
+ sortOrder,
+ workItemType,
+ discussionFilter,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Should have the Activity label', () => {
+ expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel);
+ });
+
+ it('Should have Activity filtering dropdown', () => {
+ expect(findActivityFilterDropdown().exists()).toBe(true);
+ });
+
+ it('Should have Activity sorting dropdown', () => {
+ expect(findActivitySortDropdown().exists()).toBe(true);
+ });
+
+ describe('Activity Filter', () => {
+ it('emits `changeFilter` when filtering discussions', () => {
+ findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
+ });
+ });
+
+ describe('Activity Sorting', () => {
+ it('emits `changeSort` when sorting discussions/activity', () => {
+ findActivitySortDropdown().vm.$emit('changeSort', ASC);
+
+ expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
+ });
+ });
+});
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 3c312fb4552..a0db8172bf6 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -52,10 +52,6 @@ describe('WorkItemActions component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders modal', () => {
createComponent();
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 e85f62b881d..2a8159f7294 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -113,10 +113,6 @@ describe('WorkItemAssignees component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('passes the correct data-user-id attribute', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
index 0ab2546440b..4f1d49ee2e5 100644
--- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -29,10 +29,6 @@ describe('WorkItemDescription', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders gfm', async () => {
createComponent();
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 a12ec23c15a..b4b7b8989ea 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -99,10 +99,6 @@ describe('WorkItemDescription', () => {
}
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('editing description with workItemsMvc FF enabled', () => {
beforeEach(() => {
workItemsMvc = true;
@@ -117,11 +113,14 @@ describe('WorkItemDescription', () => {
await createComponent({ isEditing: true });
expect(findMarkdownEditor().props()).toMatchObject({
- autocompleteDataSources: autocompleteDataSources(fullPath, iid),
supportsQuickActions: true,
renderMarkdownPath: markdownPreviewPath(fullPath, iid),
quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
});
+
+ expect(findMarkdownEditor().vm.$attrs).toMatchObject({
+ 'autocomplete-data-sources': autocompleteDataSources(fullPath, iid),
+ });
});
});
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 938cf6e6f51..1bdf5d1c840 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
@@ -82,10 +82,6 @@ describe('WorkItemDetailModal component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders WorkItemDetail', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 64a7502671e..fe7556f8ec6 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -99,9 +99,7 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
- workItemsMvcEnabled = false,
workItemsMvc2Enabled = false,
- fetchByIid = false,
} = {}) => {
const handlers = [
[workItemQuery, handler],
@@ -124,9 +122,7 @@ describe('WorkItemDetail component', () => {
},
provide: {
glFeatures: {
- workItemsMvc: workItemsMvcEnabled,
workItemsMvc2: workItemsMvc2Enabled,
- useIidInWorkItemsPath: fetchByIid,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
@@ -149,7 +145,6 @@ describe('WorkItemDetail component', () => {
};
afterEach(() => {
- wrapper.destroy();
setWindowLocation('');
});
@@ -420,6 +415,12 @@ describe('WorkItemDetail component', () => {
expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
});
+ it('shows parent title and iid', () => {
+ expect(findParentButton().text()).toBe(
+ `${mockParent.parent.title} #${mockParent.parent.iid}`,
+ );
+ });
+
it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
expect(findParentButton().attributes().href).toBe('../../issues/5');
});
@@ -441,6 +442,11 @@ describe('WorkItemDetail component', () => {
expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl);
});
+
+ it('shows work item type and iid', () => {
+ const { iid, workItemType } = workItemQueryResponse.data.workItem;
+ expect(findParent().text()).toContain(`${workItemType.name} #${iid}`);
+ });
});
});
@@ -626,7 +632,7 @@ describe('WorkItemDetail component', () => {
});
});
- it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
+ it('calls the global ID work item query when there is no `iid_path` parameter in URL', async () => {
createComponent();
await waitForPromises();
@@ -636,20 +642,10 @@ describe('WorkItemDetail component', () => {
expect(successByIidHandler).not.toHaveBeenCalled();
});
- it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => {
- createComponent({ fetchByIid: true });
- await waitForPromises();
-
- expect(successHandler).toHaveBeenCalledWith({
- id: workItemQueryResponse.data.workItem.id,
- });
- expect(successByIidHandler).not.toHaveBeenCalled();
- });
-
- it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
+ it('calls the IID work item query when `iid_path` route parameter is present', async () => {
setWindowLocation(`?iid_path=true`);
- createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
+ createComponent();
await waitForPromises();
expect(successHandler).not.toHaveBeenCalled();
@@ -659,10 +655,10 @@ describe('WorkItemDetail component', () => {
});
});
- it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present and is a modal', async () => {
+ it('calls the IID work item query when `iid_path` route parameter is present and is a modal', async () => {
setWindowLocation(`?iid_path=true`);
- createComponent({ fetchByIid: true, iidPathQueryParam: 'true', isModal: true });
+ createComponent({ isModal: true });
await waitForPromises();
expect(successHandler).not.toHaveBeenCalled();
@@ -748,21 +744,10 @@ describe('WorkItemDetail component', () => {
});
describe('notes widget', () => {
- it('does not render notes by default', async () => {
+ it('renders notes by default', async () => {
createComponent();
await waitForPromises();
- expect(findNotesWidget().exists()).toBe(false);
- });
-
- it('renders notes when the work_items_mvc flag is on', async () => {
- const notesWorkItem = workItemResponseFactory({
- notesWidgetPresent: true,
- });
- const handler = jest.fn().mockResolvedValue(notesWorkItem);
- createComponent({ workItemsMvcEnabled: true, handler });
- await waitForPromises();
-
expect(findNotesWidget().exists()).toBe(true);
});
});
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
index 7ebaf8209c7..b4811db8bed 100644
--- a/spec/frontend/work_items/components/work_item_due_date_spec.js
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -46,10 +46,6 @@ describe('WorkItemDueDate component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when can update', () => {
describe('start date', () => {
describe('`Add start date` button', () => {
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 0b6ab5c3290..6d51448194b 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -72,10 +72,6 @@ describe('WorkItemLabels component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a label', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
index 5fbd8e7e1a7..688dccbda79 100644
--- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
@@ -15,10 +15,6 @@ describe('RelatedItemsTree', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('OkrActionsSplitButton', () => {
describe('template', () => {
it('renders objective and key results sections', () => {
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
index 0470249d7ce..721436e217e 100644
--- 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
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
@@ -31,7 +31,7 @@ import {
workItemObjectiveMetadataWidgets,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('WorkItemLinkChild', () => {
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
@@ -67,10 +67,6 @@ describe('WorkItemLinkChild', () => {
createAlert.mockClear();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
@@ -109,10 +105,30 @@ describe('WorkItemLinkChild', () => {
});
it('renders item title', () => {
- expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
+ expect(titleEl.attributes('href')).toBe(
+ '/gitlab-org/gitlab-test/-/work_items/4?iid_path=true',
+ );
expect(titleEl.text()).toBe(workItemTask.title);
});
+ describe('renders item title correctly for relative instance', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/test' };
+ createComponent();
+ titleEl = wrapper.findByTestId('item-title');
+ });
+
+ it('renders item title with correct href', () => {
+ expect(titleEl.attributes('href')).toBe(
+ '/test/gitlab-org/gitlab-test/-/work_items/4?iid_path=true',
+ );
+ });
+
+ it('renders item title with correct text', () => {
+ expect(titleEl.text()).toBe(workItemTask.title);
+ });
+ });
+
it.each`
action | event | emittedEvent
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
@@ -149,6 +165,8 @@ describe('WorkItemLinkChild', () => {
expect(metadataEl.props()).toMatchObject({
metadataWidgets: workItemObjectiveMetadataWidgets,
});
+
+ expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-3');
});
it('does not render item metadata component when item has no metadata present', () => {
@@ -158,6 +176,8 @@ describe('WorkItemLinkChild', () => {
});
expect(findMetadataComponent().exists()).toBe(false);
+
+ expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-0');
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 480f8fbcc58..5184b24d202 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -75,10 +75,6 @@ describe('WorkItemLinksForm', () => {
const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('creating a new work item', () => {
beforeEach(async () => {
await createComponent();
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
index e3f3b74f296..4e53fc2987b 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
@@ -17,10 +17,6 @@ describe('WorkItemLinksMenu', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders dropdown and dropdown items', () => {
expect(findDropdown().exists()).toBe(true);
expect(findRemoveDropdownItem().exists()).toBe(true);
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 ec51f92b578..99e44b4d89c 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
@@ -54,7 +54,6 @@ describe('WorkItemLinks', () => {
mutationHandler = mutationChangeParentHandler,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
- fetchByIid = false,
} = {}) => {
mockApollo = createMockApollo(
[
@@ -76,11 +75,7 @@ describe('WorkItemLinks', () => {
},
provide: {
projectPath: 'project/path',
- iid: '1',
hasIterationsFeature,
- glFeatures: {
- useIidInWorkItemsPath: fetchByIid,
- },
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -351,7 +346,7 @@ describe('WorkItemLinks', () => {
beforeEach(async () => {
setWindowLocation('?iid_path=true');
- await createComponent({ fetchByIid: true });
+ await createComponent();
firstChild = findFirstWorkItemLinkChild();
});
@@ -391,7 +386,7 @@ describe('WorkItemLinks', () => {
it('starts prefetching work item by iid if URL contains work item id', async () => {
setWindowLocation('?work_item_iid=5&iid_path=true');
- await createComponent({ fetchByIid: true });
+ await createComponent();
expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
iid: '5',
@@ -402,7 +397,7 @@ describe('WorkItemLinks', () => {
it('does not open the modal if work item iid URL parameter is not found in child items', async () => {
setWindowLocation('?work_item_iid=555&iid_path=true');
- await createComponent({ fetchByIid: true });
+ await createComponent();
expect(showModal).not.toHaveBeenCalled();
expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null);
@@ -410,7 +405,7 @@ describe('WorkItemLinks', () => {
it('opens the modal if work item iid URL parameter is found in child items', async () => {
setWindowLocation('?work_item_iid=2&iid_path=true');
- await createComponent({ fetchByIid: true });
+ await createComponent();
expect(showModal).toHaveBeenCalled();
expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2');
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 3db848a0ad2..a067923b9fc 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -9,10 +9,13 @@ import SystemNote from '~/work_items/components/notes/system_note.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
+import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql';
+import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql';
+import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
import {
@@ -21,6 +24,9 @@ import {
mockWorkItemNotesByIidResponse,
mockMoreWorkItemNotesResponse,
mockWorkItemNotesResponseWithComments,
+ workItemNotesCreateSubscriptionResponse,
+ workItemNotesUpdateSubscriptionResponse,
+ workItemNotesDeleteSubscriptionResponse,
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
@@ -53,10 +59,9 @@ describe('WorkItemNotes component', () => {
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
const findAllListItems = () => wrapper.findAll('ul.timeline > *');
- const findActivityLabel = () => wrapper.find('label');
const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
+ const findActivityHeader = () => wrapper.findComponent(WorkItemNotesActivityHeader);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
@@ -73,6 +78,15 @@ describe('WorkItemNotes component', () => {
const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
});
+ const notesCreateSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemNotesCreateSubscriptionResponse);
+ const notesUpdateSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemNotesUpdateSubscriptionResponse);
+ const notesDeleteSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemNotesDeleteSubscriptionResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
@@ -86,6 +100,9 @@ describe('WorkItemNotes component', () => {
[workItemNotesQuery, defaultWorkItemNotesQueryHandler],
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
[deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
+ [workItemNoteCreatedSubscription, notesCreateSubscriptionHandler],
+ [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler],
+ [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler],
]),
propsData: {
workItemId,
@@ -96,11 +113,6 @@ describe('WorkItemNotes component', () => {
fetchByIid,
workItemType: 'task',
},
- provide: {
- glFeatures: {
- useIidInWorkItemsPath: fetchByIid,
- },
- },
stubs: {
GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
},
@@ -111,8 +123,8 @@ describe('WorkItemNotes component', () => {
createComponent();
});
- it('renders activity label', () => {
- expect(findActivityLabel().exists()).toBe(true);
+ it('has the work item note activity header', () => {
+ expect(findActivityHeader().exists()).toBe(true);
});
it('passes correct props to comment form component', async () => {
@@ -203,26 +215,22 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
});
- it('filter exists', () => {
- expect(findSortingFilter().exists()).toBe(true);
- });
-
- it('sorts the list when the `changeSortOrder` event is emitted', async () => {
+ it('sorts the list when the `changeSort` event is emitted', async () => {
expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId);
- await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+ await findActivityHeader().vm.$emit('changeSort', DESC);
expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
});
it('puts form at start of list in when sorting by newest first', async () => {
- await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+ await findActivityHeader().vm.$emit('changeSort', DESC);
expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
});
it('puts form at end of list in when sorting by oldest first', async () => {
- await findSortingFilter().vm.$emit('changeSortOrder', ASC);
+ await findActivityHeader().vm.$emit('changeSort', ASC);
expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
});
@@ -334,4 +342,31 @@ describe('WorkItemNotes component', () => {
['Something went wrong when deleting a comment. Please try again'],
]);
});
+
+ describe('Notes subscriptions', () => {
+ beforeEach(async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+ });
+
+ it('has create notes subscription', () => {
+ expect(notesCreateSubscriptionHandler).toHaveBeenCalledWith({
+ noteableId: mockWorkItemId,
+ });
+ });
+
+ it('has delete notes subscription', () => {
+ expect(notesDeleteSubscriptionHandler).toHaveBeenCalledWith({
+ noteableId: mockWorkItemId,
+ });
+ });
+
+ it('has update notes subscription', () => {
+ expect(notesUpdateSubscriptionHandler).toHaveBeenCalledWith({
+ noteableId: mockWorkItemId,
+ });
+ });
+ });
});
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 b24d940d56a..d1262057c73 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -44,10 +44,6 @@ describe('WorkItemState component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders state', () => {
createComponent();
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 a549aad5cd8..34391b74cf7 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -41,10 +41,6 @@ describe('WorkItemTitle component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title', () => {
createComponent();
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 182fb0f8cb6..a5e955c4dbf 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
@@ -9,7 +9,7 @@ function createComponent(propsData) {
wrapper = shallowMount(WorkItemTypeIcon, {
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
}
@@ -17,10 +17,6 @@ function createComponent(propsData) {
describe('Work Item type component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
workItemType | workItemIconName | iconName | text | showTooltipOnHover
${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false}
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d4832fe376d..fecf98b2651 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -82,6 +82,7 @@ export const workItemQueryResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -182,6 +183,7 @@ export const updateWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -330,6 +332,7 @@ export const workItemResponseFactory = ({
userPermissions: {
deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -473,23 +476,20 @@ export const workItemResponseFactory = ({
export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({
data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- issuable: {
- id: 'gid://gitlab/Issue/4',
- confidential,
- iteration: {
- id: 'gid://gitlab/Iteration/1124',
- __typename: 'Iteration',
- },
- milestone: {
- id: 'gid://gitlab/Milestone/28',
- __typename: 'Milestone',
- },
- __typename: 'Issue',
+ issue: {
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ __typename: 'Iteration',
},
- __typename: 'Project',
+ milestone: {
+ id: 'gid://gitlab/Milestone/28',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
},
+ __typename: 'Project',
},
});
@@ -542,6 +542,7 @@ export const createWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [],
},
@@ -590,6 +591,7 @@ export const createWorkItemFromTaskMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -630,6 +632,7 @@ export const createWorkItemFromTaskMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [],
},
@@ -831,15 +834,20 @@ export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
+ iid: 1,
+ state: 'OPEN',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/6',
+ id: 'gid://gitlab/WorkItems::Type/1',
name: 'Issue',
iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
+ description: '',
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: mockAssignees[0],
project: {
__typename: 'Project',
id: '1',
@@ -849,14 +857,11 @@ export const workItemHierarchyEmptyResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
confidential: false,
widgets: [
{
- type: 'DESCRIPTION',
- __typename: 'WorkItemWidgetDescription',
- },
- {
type: 'HIERARCHY',
parent: null,
hasChildren: false,
@@ -876,6 +881,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
+ iid: 1,
+ state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
name: 'Issue',
@@ -883,9 +890,15 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
__typename: 'WorkItemType',
},
title: 'New title',
+ description: '',
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
+ author: mockAssignees[0],
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ __typename: 'WorkItemPermissions',
},
project: {
__typename: 'Project',
@@ -896,10 +909,6 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
confidential: false,
widgets: [
{
- type: 'DESCRIPTION',
- __typename: 'WorkItemWidgetDescription',
- },
- {
type: 'HIERARCHY',
parent: null,
hasChildren: true,
@@ -952,6 +961,7 @@ export const workItemTask = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [],
__typename: 'WorkItem',
};
@@ -969,6 +979,7 @@ export const confidentialWorkItemTask = {
confidential: true,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [],
__typename: 'WorkItem',
};
@@ -986,6 +997,7 @@ export const closedWorkItemTask = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: '2022-08-12T13:07:52Z',
+ widgets: [],
__typename: 'WorkItem',
};
@@ -1007,6 +1019,7 @@ export const childrenWorkItems = [
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [],
__typename: 'WorkItem',
},
];
@@ -1017,15 +1030,19 @@ export const workItemHierarchyResponse = {
id: 'gid://gitlab/WorkItem/1',
iid: '1',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/6',
- name: 'Objective',
- iconName: 'issue-type-objective',
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ __typename: 'WorkItemPermissions',
+ },
+ author: {
+ ...mockAssignees[0],
},
confidential: false,
project: {
@@ -1034,12 +1051,13 @@ export const workItemHierarchyResponse = {
fullPath: 'test-project-path',
archived: false,
},
+ description: 'Issue description',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
widgets: [
{
- type: 'DESCRIPTION',
- __typename: 'WorkItemWidgetDescription',
- },
- {
type: 'HIERARCHY',
parent: null,
hasChildren: true,
@@ -1110,6 +1128,7 @@ export const workItemObjectiveWithChild = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ __typename: 'WorkItemPermissions',
},
author: {
...mockAssignees[0],
@@ -1176,6 +1195,7 @@ export const workItemHierarchyTreeResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ __typename: 'WorkItemPermissions',
},
confidential: false,
project: {
@@ -1252,6 +1272,7 @@ export const changeWorkItemParentMutationResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ __typename: 'WorkItemPermissions',
},
description: null,
id: 'gid://gitlab/WorkItem/2',
@@ -1624,6 +1645,7 @@ export const projectWorkItemResponse = {
workItems: {
nodes: [workItemQueryResponse.data.workItem],
},
+ __typename: 'Project',
},
},
};
@@ -1681,6 +1703,8 @@ export const mockWorkItemNotesResponse = {
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_199',
lastEditedBy: null,
system: true,
internal: false,
@@ -1724,6 +1748,8 @@ export const mockWorkItemNotesResponse = {
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_201',
lastEditedBy: null,
system: true,
internal: false,
@@ -1766,6 +1792,8 @@ export const mockWorkItemNotesResponse = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_202',
lastEditedBy: null,
system: true,
internal: false,
@@ -1868,6 +1896,8 @@ export const mockWorkItemNotesByIidResponse = {
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -1913,6 +1943,8 @@ export const mockWorkItemNotesByIidResponse = {
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -1959,6 +1991,8 @@ export const mockWorkItemNotesByIidResponse = {
systemNoteIconName: 'iteration',
createdAt: '2022-11-14T04:19:00Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2059,6 +2093,8 @@ export const mockMoreWorkItemNotesResponse = {
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2102,6 +2138,8 @@ export const mockMoreWorkItemNotesResponse = {
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2144,6 +2182,8 @@ export const mockMoreWorkItemNotesResponse = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2205,6 +2245,7 @@ export const createWorkItemNoteResponse = {
systemNoteIconName: null,
createdAt: '2023-01-25T04:49:46Z',
lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
discussion: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
@@ -2252,6 +2293,7 @@ export const mockWorkItemCommentNote = {
systemNoteIconName: false,
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: false,
internal: false,
@@ -2331,6 +2373,8 @@ export const mockWorkItemNotesResponseWithComments = {
systemNoteIconName: null,
createdAt: '2023-01-12T07:47:40Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
discussion: {
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
@@ -2365,6 +2409,8 @@ export const mockWorkItemNotesResponseWithComments = {
systemNoteIconName: null,
createdAt: '2023-01-18T09:09:54Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
discussion: {
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
@@ -2406,6 +2452,8 @@ export const mockWorkItemNotesResponseWithComments = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
+ url:
+ 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
lastEditedBy: null,
system: false,
internal: false,
@@ -2447,3 +2495,129 @@ export const mockWorkItemNotesResponseWithComments = {
},
},
};
+
+export const workItemNotesCreateSubscriptionResponse = {
+ data: {
+ workItemNoteCreated: {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d81864',
+ body: 'changed weight to **89**',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9881864',
+ body: 'changed weight to **89**',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ },
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ },
+};
+
+export const workItemNotesUpdateSubscriptionResponse = {
+ data: {
+ workItemNoteUpdated: {
+ id: 'gid://gitlab/Note/0f2f195ec0d1ef95ee9d5b10446b8e96a9883894',
+ body: 'changed title',
+ bodyHtml: '<p dir="auto">changed title<strong>89</strong></p>',
+ systemNoteIconName: 'pencil',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ },
+};
+
+export const workItemNotesDeleteSubscriptionResponse = {
+ data: {
+ workItemNoteDeleted: {
+ id: 'gid://gitlab/DiscussionNote/235',
+ discussionId: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ lastDiscussionNote: false,
+ },
+ },
+};
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 387c8a355fa..b963f041dd9 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -37,7 +37,6 @@ describe('Create work item component', () => {
props = {},
queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler,
- fetchByIid = false,
} = {}) => {
fakeApollo = createMockApollo(
[
@@ -66,15 +65,11 @@ describe('Create work item component', () => {
},
provide: {
fullPath: 'full-path',
- glFeatures: {
- useIidInWorkItemsPath: fetchByIid,
- },
},
});
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -109,9 +104,8 @@ describe('Create work item component', () => {
expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
name: 'workItem',
- params: {
- id: '1',
- },
+ params: { id: '1' },
+ query: { iid_path: 'true' },
});
});
@@ -210,18 +204,4 @@ describe('Create work item component', () => {
'Something went wrong when creating work item. Please try again.',
);
});
-
- it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => {
- createComponent({ fetchByIid: true });
- findTitleInput().vm.$emit('title-input', 'Test title');
-
- wrapper.find('form').trigger('submit');
- await waitForPromises();
-
- expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
- name: 'workItem',
- params: { id: '1' },
- query: { iid_path: 'true' },
- });
- });
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index a766962771a..37326910e13 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -44,10 +44,6 @@ describe('Work items root component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders WorkItemDetail', () => {
createComponent();
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index ef9ae4a2eab..5dad7f7c43f 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -75,6 +75,7 @@ describe('Work items router', () => {
WorkItemWeight: true,
WorkItemIteration: true,
WorkItemHealthStatus: true,
+ WorkItemNotes: true,
},
});
};
@@ -88,6 +89,7 @@ describe('Work items router', () => {
});
afterEach(() => {
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
window.location.hash = '';
});
diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js
index 124ff5f1608..22fd7d5f48a 100644
--- a/spec/frontend/work_items_hierarchy/components/app_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/app_spec.js
@@ -24,10 +24,6 @@ describe('WorkItemsHierarchy App', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('survey banner', () => {
it('shows when the banner is visible', () => {
createComponent({}, { bannerVisible: true });
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
index 084aaa754ab..dfdef7915dd 100644
--- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -40,10 +40,6 @@ describe('WorkItemsHierarchy Hierarchy', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('available structure', () => {
let items = [];
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index 85f1dbdc305..025a92464f1 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -15,6 +15,8 @@ describe('ZenMode', () => {
let dropzoneForElementSpy;
const fixtureName = 'snippets/show.html';
+ const getTextarea = () => $('.notes-form textarea');
+
function enterZen() {
$('.notes-form .js-zen-enter').click();
}
@@ -24,7 +26,7 @@ describe('ZenMode', () => {
}
function escapeKeydown() {
- $('.notes-form textarea').trigger(
+ getTextarea().trigger(
$.Event('keydown', {
keyCode: 27,
}),
@@ -50,6 +52,12 @@ describe('ZenMode', () => {
});
afterEach(() => {
+ $(document).off('click', '.js-zen-enter');
+ $(document).off('click', '.js-zen-leave');
+ $(document).off('zen_mode:enter');
+ $(document).off('zen_mode:leave');
+ $(document).off('keydown');
+
resetHTMLFixture();
});
@@ -62,14 +70,14 @@ describe('ZenMode', () => {
$('.div-dropzone').addClass('js-invalid-dropzone');
exitZen();
- expect(dropzoneForElementSpy.mock.calls.length).toEqual(0);
+ expect(dropzoneForElementSpy).not.toHaveBeenCalled();
});
it('should call dropzone if element is dropzone valid', () => {
$('.div-dropzone').removeClass('js-invalid-dropzone');
exitZen();
- expect(dropzoneForElementSpy.mock.calls.length).toEqual(2);
+ expect(dropzoneForElementSpy).toHaveBeenCalledTimes(1);
});
});
@@ -82,10 +90,10 @@ describe('ZenMode', () => {
});
it('removes textarea styling', () => {
- $('.notes-form textarea').attr('style', 'height: 400px');
+ getTextarea().attr('style', 'height: 400px');
enterZen();
- expect($('.notes-form textarea')).not.toHaveAttr('style');
+ expect(getTextarea()).not.toHaveAttr('style');
});
});
@@ -116,4 +124,15 @@ describe('ZenMode', () => {
expect(utils.scrollToElement).toHaveBeenCalled();
});
});
+
+ it('restores textarea style', () => {
+ const style = 'color: red; overflow-y: hidden;';
+ getTextarea().attr('style', style);
+ expect(getTextarea()).toHaveAttr('style', style);
+
+ enterZen();
+ exitZen();
+
+ expect(getTextarea()).toHaveAttr('style', style);
+ });
});
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 8521e85a971..a80c4db19b5 100644
--- a/spec/frontend_integration/content_editor/content_editor_integration_spec.js
+++ b/spec/frontend_integration/content_editor/content_editor_integration_spec.js
@@ -38,10 +38,6 @@ describe('content_editor', () => {
renderMarkdown = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading initial content', () => {
describe('when the initial content is empty', () => {
it('still hides the loading indicator', async () => {
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js
index a6108fd71e1..5711b004f70 100644
--- a/spec/frontend_integration/ide/ide_integration_spec.js
+++ b/spec/frontend_integration/ide/ide_integration_spec.js
@@ -22,7 +22,6 @@ describe('WebIDE', () => {
afterEach(() => {
vm.$destroy();
- vm = null;
resetHTMLFixture();
});
diff --git a/spec/frontend_integration/ide/user_opens_file_spec.js b/spec/frontend_integration/ide/user_opens_file_spec.js
index af6e2f3b44b..93c9fff309f 100644
--- a/spec/frontend_integration/ide/user_opens_file_spec.js
+++ b/spec/frontend_integration/ide/user_opens_file_spec.js
@@ -23,7 +23,6 @@ describe('IDE: User opens a file in the Web IDE', () => {
afterEach(() => {
vm.$destroy();
- vm = null;
resetHTMLFixture();
});
diff --git a/spec/frontend_integration/ide/user_opens_ide_spec.js b/spec/frontend_integration/ide/user_opens_ide_spec.js
index 552888f04a5..d4656b1098e 100644
--- a/spec/frontend_integration/ide/user_opens_ide_spec.js
+++ b/spec/frontend_integration/ide/user_opens_ide_spec.js
@@ -20,7 +20,6 @@ describe('IDE: User opens IDE', () => {
afterEach(() => {
vm.$destroy();
- vm = null;
resetHTMLFixture();
});
diff --git a/spec/frontend_integration/ide/user_opens_mr_spec.js b/spec/frontend_integration/ide/user_opens_mr_spec.js
index af0276a5055..4e90cef6016 100644
--- a/spec/frontend_integration/ide/user_opens_mr_spec.js
+++ b/spec/frontend_integration/ide/user_opens_mr_spec.js
@@ -34,7 +34,6 @@ describe('IDE: User opens Merge Request', () => {
afterEach(() => {
vm.$destroy();
- vm = null;
resetHTMLFixture();
});
diff --git a/spec/graphql/mutations/achievements/award_spec.rb b/spec/graphql/mutations/achievements/award_spec.rb
new file mode 100644
index 00000000000..1bfad46a616
--- /dev/null
+++ b/spec/graphql/mutations/achievements/award_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:recipient) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+
+ describe '#resolve' do
+ subject(:resolve_mutation) do
+ described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
+ achievement_id: achievement&.to_global_id, user_id: recipient&.to_global_id
+ )
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ expect { resolve_mutation }.to raise_error { Gitlab::Graphql::Errors::ArgumentError }
+ end
+ end
+
+ it 'creates user_achievement with correct values' do
+ expect(resolve_mutation[:user_achievement]).to have_attributes({ achievement: achievement, user: recipient })
+ end
+ end
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:award_achievement) }
+end
diff --git a/spec/graphql/mutations/achievements/revoke_spec.rb b/spec/graphql/mutations/achievements/revoke_spec.rb
new file mode 100644
index 00000000000..0c221b492af
--- /dev/null
+++ b/spec/graphql/mutations/achievements/revoke_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Revoke, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:recipient) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ describe '#resolve' do
+ subject(:resolve_mutation) do
+ described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve(
+ user_achievement_id: user_achievement&.to_global_id
+ )
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it 'raises an error' do
+ expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:user_achievement) { nil }
+
+ it 'returns the validation error' do
+ expect { resolve_mutation }.to raise_error { Gitlab::Graphql::Errors::ArgumentError }
+ end
+ end
+
+ it 'revokes user_achievement' do
+ response = resolve_mutation[:user_achievement]
+
+ expect(response.revoked_at).not_to be_nil
+ expect(response.revoked_by_user_id).to be(current_user.id)
+ end
+ end
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:award_achievement) }
+end
diff --git a/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
index 125e15b70cf..da5531d2b93 100644
--- a/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
+++ b/spec/graphql/mutations/alert_management/alerts/set_assignees_spec.rb
@@ -58,7 +58,6 @@ RSpec.describe Mutations::AlertManagement::Alerts::SetAssignees do
it_behaves_like 'an incident management tracked event', :incident_management_alert_assigned
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
diff --git a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
index bcb7c74fa09..8ba1e785b63 100644
--- a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
+++ b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
@@ -20,7 +20,6 @@ RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do
it_behaves_like 'an incident management tracked event', :incident_management_alert_todo
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
diff --git a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
index e49596b37c9..f86046bb0d6 100644
--- a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
+++ b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
@@ -32,7 +32,6 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
it_behaves_like 'an incident management tracked event', :incident_management_alert_create_incident
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
@@ -57,7 +56,6 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
index 22ad93df79b..fb11ec7065b 100644
--- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
+++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
@@ -36,7 +36,6 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace }
let(:category) { described_class.to_s }
let(:user) { current_user }
diff --git a/spec/graphql/mutations/members/bulk_update_base_spec.rb b/spec/graphql/mutations/members/bulk_update_base_spec.rb
new file mode 100644
index 00000000000..61a27984824
--- /dev/null
+++ b/spec/graphql/mutations/members/bulk_update_base_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Members::BulkUpdateBase, feature_category: :subgroups do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group).tap { |group| group.add_owner(user) } }
+
+ it 'raises a NotImplementedError error if the source_type method is called on the base class' do
+ mutation = described_class.new(context: { current_user: user }, object: nil, field: nil)
+
+ expect { mutation.resolve(group_id: group.to_gid.to_s) }.to raise_error(NotImplementedError)
+ end
+end
diff --git a/spec/graphql/mutations/release_asset_links/create_spec.rb b/spec/graphql/mutations/release_asset_links/create_spec.rb
index a5291a00799..cc6c1554866 100644
--- a/spec/graphql/mutations/release_asset_links/create_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::ReleaseAssetLinks::Create do
+RSpec.describe Mutations::ReleaseAssetLinks::Create, feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
diff --git a/spec/graphql/mutations/release_asset_links/delete_spec.rb b/spec/graphql/mutations/release_asset_links/delete_spec.rb
index cca7bd2ba38..3aecc44afd1 100644
--- a/spec/graphql/mutations/release_asset_links/delete_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::ReleaseAssetLinks::Delete do
+RSpec.describe Mutations::ReleaseAssetLinks::Delete, feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
@@ -60,6 +60,18 @@ RSpec.describe Mutations::ReleaseAssetLinks::Delete do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
+
+ context 'when destroy process fails' do
+ before do
+ allow_next_instance_of(::Releases::Links::DestroyService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it 'returns errors' do
+ expect(resolve).to include(errors: 'error')
+ end
+ end
end
context 'when the current user does not have access to delete the link' do
diff --git a/spec/graphql/mutations/release_asset_links/update_spec.rb b/spec/graphql/mutations/release_asset_links/update_spec.rb
index e119cf9cc77..abb091fc68d 100644
--- a/spec/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/update_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::ReleaseAssetLinks::Update do
+RSpec.describe Mutations::ReleaseAssetLinks::Update, feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private, :repository) }
diff --git a/spec/graphql/resolvers/achievements/achievements_resolver_spec.rb b/spec/graphql/resolvers/achievements/achievements_resolver_spec.rb
new file mode 100644
index 00000000000..666610dca33
--- /dev/null
+++ b/spec/graphql/resolvers/achievements/achievements_resolver_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Achievements::AchievementsResolver, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+
+ specify do
+ expect(described_class).to have_nullable_graphql_type(Types::Achievements::AchievementType.connection_type)
+ end
+
+ describe '#resolve' do
+ it 'is not empty' do
+ expect(resolve_achievements).not_to be_empty
+ end
+
+ context 'when `achievements` feature flag is diabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'is empty' do
+ expect(resolve_achievements).to be_empty
+ end
+ end
+ end
+
+ def resolve_achievements
+ resolve(described_class, obj: group)
+ end
+end
diff --git a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
index 5d06db904d5..ff343f3f43d 100644
--- a/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe Resolvers::Ci::GroupRunnersResolver, feature_category: :runner_fl
status_status: 'active',
type_type: :group_type,
tag_name: ['active_runner'],
- preload: { tag_name: false },
+ preload: false,
search: 'abc',
sort: 'contacted_asc',
membership: :descendants,
diff --git a/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb b/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb
index 4cc00ced104..83435db2ea7 100644
--- a/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/project_runners_resolver_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Resolvers::Ci::ProjectRunnersResolver, feature_category: :runner_
status_status: 'active',
type_type: :group_type,
tag_name: ['active_runner'],
- preload: { tag_name: false },
+ preload: false,
search: 'abc',
sort: 'contacted_asc',
project: project
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index d6da8222234..e4620b96cae 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -83,7 +83,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
upgrade_status: 'recommended',
type_type: :instance_type,
tag_name: ['active_runner'],
- preload: { tag_name: false },
+ preload: false,
search: 'abc',
sort: 'contacted_asc'
}
@@ -108,7 +108,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
let(:expected_params) do
{
active: false,
- preload: { tag_name: false }
+ preload: false
}
end
@@ -128,7 +128,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
let(:expected_params) do
{
active: false,
- preload: { tag_name: false }
+ preload: false
}
end
@@ -146,9 +146,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver, feature_category: :runner_fleet d
end
let(:expected_params) do
- {
- preload: { tag_name: false }
- }
+ { preload: false }
end
it 'calls RunnersFinder with expected arguments' do
diff --git a/spec/graphql/resolvers/ci/variables_resolver_spec.rb b/spec/graphql/resolvers/ci/variables_resolver_spec.rb
index 16b72e8cb7f..1bfc63df71d 100644
--- a/spec/graphql/resolvers/ci/variables_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/variables_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Ci::VariablesResolver, feature_category: :pipeline_authoring do
+RSpec.describe Resolvers::Ci::VariablesResolver, feature_category: :pipeline_composition do
include GraphqlHelpers
describe '#resolve' do
diff --git a/spec/graphql/types/achievements/achievement_type_spec.rb b/spec/graphql/types/achievements/achievement_type_spec.rb
index f967dc8e25e..08fadcdff22 100644
--- a/spec/graphql/types/achievements/achievement_type_spec.rb
+++ b/spec/graphql/types/achievements/achievement_type_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Achievement'], feature_category: :user_profil
description
created_at
updated_at
+ user_achievements
]
end
diff --git a/spec/graphql/types/achievements/user_achievement_type_spec.rb b/spec/graphql/types/achievements/user_achievement_type_spec.rb
new file mode 100644
index 00000000000..6b1512ff841
--- /dev/null
+++ b/spec/graphql/types/achievements/user_achievement_type_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['UserAchievement'], feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let(:fields) do
+ %w[
+ id
+ achievement
+ user
+ awarded_by_user
+ revoked_by_user
+ created_at
+ updated_at
+ revoked_at
+ ]
+ end
+
+ it { expect(described_class.graphql_name).to eq('UserAchievement') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+ it { expect(described_class).to require_graphql_authorizations(:read_achievement) }
+end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index 714eaebfe73..a761a256899 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::JobType do
+RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do
include GraphqlHelpers
specify { expect(described_class.graphql_name).to eq('CiJob') }
@@ -40,6 +40,7 @@ RSpec.describe Types::Ci::JobType do
refPath
retryable
retried
+ runnerMachine
scheduledAt
schedulingType
shortSha
@@ -51,6 +52,9 @@ RSpec.describe Types::Ci::JobType do
triggered
userPermissions
webPath
+ playPath
+ canPlayJob
+ scheduled
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/runner_machine_type_spec.rb b/spec/graphql/types/ci/runner_machine_type_spec.rb
new file mode 100644
index 00000000000..289cc52e27b
--- /dev/null
+++ b/spec/graphql/types/ci/runner_machine_type_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiRunnerMachine'], feature_category: :runner_fleet do
+ specify { expect(described_class.graphql_name).to eq('CiRunnerMachine') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_runner_machine) }
+
+ it 'contains attributes related to a runner machine' do
+ expected_fields = %w[
+ architecture_name contacted_at created_at executor_name id ip_address platform_name revision
+ runner status system_id version
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb
index a2d107ae295..9e360f44a4f 100644
--- a/spec/graphql/types/ci/runner_type_spec.rb
+++ b/spec/graphql/types/ci/runner_type_spec.rb
@@ -9,11 +9,11 @@ RSpec.describe GitlabSchema.types['CiRunner'], feature_category: :runner do
it 'contains attributes related to a runner' do
expected_fields = %w[
- id description created_at contacted_at maximum_timeout access_level active paused status
+ id description created_by created_at contacted_at machines maximum_timeout access_level active paused status
version short_sha revision locked run_untagged ip_address runner_type tag_list
- project_count job_count admin_url edit_admin_url user_permissions executor_name architecture_name platform_name
- maintenance_note maintenance_note_html groups projects jobs token_expires_at owner_project job_execution_status
- ephemeral_authentication_token
+ project_count job_count admin_url edit_admin_url register_admin_url user_permissions executor_name
+ architecture_name platform_name maintenance_note maintenance_note_html groups projects jobs token_expires_at
+ owner_project job_execution_status ephemeral_authentication_token
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/variable_sort_enum_spec.rb b/spec/graphql/types/ci/variable_sort_enum_spec.rb
index 1702360a21f..0a86597b70d 100644
--- a/spec/graphql/types/ci/variable_sort_enum_spec.rb
+++ b/spec/graphql/types/ci/variable_sort_enum_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::VariableSortEnum, feature_category: :pipeline_authoring do
+RSpec.describe Types::Ci::VariableSortEnum, feature_category: :pipeline_composition do
it 'exposes the available order methods' do
expect(described_class.values).to match(
'KEY_ASC' => have_attributes(value: :key_asc),
diff --git a/spec/graphql/types/commit_signature_interface_spec.rb b/spec/graphql/types/commit_signature_interface_spec.rb
index 4962131d9b5..d37c0d1b4fa 100644
--- a/spec/graphql/types/commit_signature_interface_spec.rb
+++ b/spec/graphql/types/commit_signature_interface_spec.rb
@@ -18,6 +18,11 @@ RSpec.describe GitlabSchema.types['CommitSignature'] do
Types::CommitSignatures::X509SignatureType)
end
+ it 'resolves SSH signatures' do
+ expect(described_class.resolve_type(build(:ssh_signature), {})).to eq(
+ Types::CommitSignatures::SshSignatureType)
+ end
+
it 'raises an error when type is not known' do
expect { described_class.resolve_type(Class, {}) }.to raise_error('Unsupported commit signature type')
end
diff --git a/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb b/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb
index 4ffb70a0b22..c16e29312a3 100644
--- a/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb
+++ b/spec/graphql/types/commit_signatures/ssh_signature_type_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe GitlabSchema.types['SshSignature'], feature_category: :source_cod
it 'contains attributes related to SSH signatures' do
expect(described_class).to have_graphql_fields(
- :user, :verification_status, :commit_sha, :project, :key
+ :user, :verification_status, :commit_sha, :project, :key, :key_fingerprint_sha256
)
end
end
diff --git a/spec/graphql/types/design_management/design_at_version_type_spec.rb b/spec/graphql/types/design_management/design_at_version_type_spec.rb
index 4d61ecf62cc..06aefb6fea3 100644
--- a/spec/graphql/types/design_management/design_at_version_type_spec.rb
+++ b/spec/graphql/types/design_management/design_at_version_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['DesignAtVersion'] do
+RSpec.describe GitlabSchema.types['DesignAtVersion'], feature_category: :portfolio_management do
it_behaves_like 'a GraphQL type with design fields' do
let(:extra_design_fields) { %i[version design] }
let_it_be(:design) { create(:design, :with_versions) }
diff --git a/spec/graphql/types/design_management/design_type_spec.rb b/spec/graphql/types/design_management/design_type_spec.rb
index 24b007a6b33..a093f785eb7 100644
--- a/spec/graphql/types/design_management/design_type_spec.rb
+++ b/spec/graphql/types/design_management/design_type_spec.rb
@@ -2,13 +2,16 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Design'] do
+RSpec.describe GitlabSchema.types['Design'], feature_category: :portfolio_management do
specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) }
specify { expect(described_class.interfaces).to include(Types::TodoableInterface) }
it_behaves_like 'a GraphQL type with design fields' do
- let(:extra_design_fields) { %i[notes current_user_todos discussions versions web_url commenters] }
+ let(:extra_design_fields) do
+ %i[notes current_user_todos discussions versions web_url commenters description descriptionHtml]
+ end
+
let_it_be(:design) { create(:design, :with_versions) }
let(:object_id) { GitlabSchema.id_from_object(design) }
let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) }
diff --git a/spec/graphql/types/key_type_spec.rb b/spec/graphql/types/key_type_spec.rb
index 78144076467..13c00d94b37 100644
--- a/spec/graphql/types/key_type_spec.rb
+++ b/spec/graphql/types/key_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Key'], feature_category: :authentication_and_authorization do
+RSpec.describe GitlabSchema.types['Key'], feature_category: :system_access do
specify { expect(described_class.graphql_name).to eq('Key') }
it 'contains attributes for SSH keys' do
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 7f26190830e..0bfca9a290b 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -291,7 +291,7 @@ RSpec.describe GitlabSchema.types['Project'] do
let_it_be(:project) { create(:project_empty_repo) }
it 'raises an error' do
- expect(subject['errors'][0]['message']).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ expect(subject['errors'][0]['message']).to eq('UF: You must <a target="_blank" rel="noopener noreferrer" ' \
'href="http://localhost/help/user/project/repository/index.md#' \
'add-files-to-a-repository">add at least one file to the ' \
'repository</a> before using Security features.')
diff --git a/spec/graphql/types/projects/fork_details_type_spec.rb b/spec/graphql/types/projects/fork_details_type_spec.rb
index 8e20e2c8299..f79371ce4ca 100644
--- a/spec/graphql/types/projects/fork_details_type_spec.rb
+++ b/spec/graphql/types/projects/fork_details_type_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe GitlabSchema.types['ForkDetails'], feature_category: :source_code
fields = %i[
ahead
behind
+ isSyncing
+ hasConflicts
]
expect(described_class).to have_graphql_fields(*fields)
diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb
index 07c8378e7a6..5dde6aa8b14 100644
--- a/spec/graphql/types/root_storage_statistics_type_spec.rb
+++ b/spec/graphql/types/root_storage_statistics_type_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['RootStorageStatistics'] do
expect(described_class).to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
:build_artifacts_size, :packages_size, :wiki_size, :snippets_size,
:pipeline_artifacts_size, :uploads_size, :dependency_proxy_size,
- :container_registry_size)
+ :container_registry_size, :registry_size_estimated)
end
specify { expect(described_class).to require_graphql_authorizations(:read_statistics) }
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index a6b5d454b60..7a0b6c90ace 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
profileEnableGitpodPath
savedReplies
savedReply
+ user_achievements
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/work_items/available_export_fields_enum_spec.rb b/spec/graphql/types/work_items/available_export_fields_enum_spec.rb
new file mode 100644
index 00000000000..5aa51160880
--- /dev/null
+++ b/spec/graphql/types/work_items/available_export_fields_enum_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['AvailableExportFields'], feature_category: :team_planning do
+ specify { expect(described_class.graphql_name).to eq('AvailableExportFields') }
+
+ describe 'enum values' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:field_name, :field_value) do
+ 'ID' | 'id'
+ 'TYPE' | 'type'
+ 'TITLE' | 'title'
+ 'AUTHOR' | 'author'
+ 'AUTHOR_USERNAME' | 'author username'
+ 'CREATED_AT' | 'created_at'
+ end
+
+ with_them do
+ it 'exposes correct available fields' do
+ expect(described_class.values[field_name].value).to eq(field_value)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/work_items/widget_interface_spec.rb b/spec/graphql/types/work_items/widget_interface_spec.rb
index a2b12ed52dc..d1dcfb961cb 100644
--- a/spec/graphql/types/work_items/widget_interface_spec.rb
+++ b/spec/graphql/types/work_items/widget_interface_spec.rb
@@ -15,11 +15,12 @@ RSpec.describe Types::WorkItems::WidgetInterface do
using RSpec::Parameterized::TableSyntax
where(:widget_class, :widget_type_name) do
- WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType
- WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType
- WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType
- WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType
- WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType
+ WorkItems::Widgets::Description | Types::WorkItems::Widgets::DescriptionType
+ WorkItems::Widgets::Hierarchy | Types::WorkItems::Widgets::HierarchyType
+ WorkItems::Widgets::Assignees | Types::WorkItems::Widgets::AssigneesType
+ WorkItems::Widgets::Labels | Types::WorkItems::Widgets::LabelsType
+ WorkItems::Widgets::Notes | Types::WorkItems::Widgets::NotesType
+ WorkItems::Widgets::Notifications | Types::WorkItems::Widgets::NotificationsType
end
with_them do
diff --git a/spec/graphql/types/work_items/widgets/notifications_type_spec.rb b/spec/graphql/types/work_items/widgets/notifications_type_spec.rb
new file mode 100644
index 00000000000..4f457a24710
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/notifications_type_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::WorkItems::Widgets::NotificationsType, feature_category: :team_planning do
+ it 'exposes the expected fields' do
+ expected_fields = %i[subscribed type]
+
+ expect(described_class.graphql_name).to eq('WorkItemWidgetNotifications')
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/work_items/widgets/notifications_update_input_type_spec.rb b/spec/graphql/types/work_items/widgets/notifications_update_input_type_spec.rb
new file mode 100644
index 00000000000..db0d02c597c
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/notifications_update_input_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Types::WorkItems::Widgets::NotificationsUpdateInputType, feature_category: :team_planning do
+ it { expect(described_class.graphql_name).to eq('WorkItemWidgetNotificationsUpdateInput') }
+
+ it { expect(described_class.arguments.keys).to contain_exactly('subscribed') }
+end
diff --git a/spec/helpers/admin/abuse_reports_helper_spec.rb b/spec/helpers/admin/abuse_reports_helper_spec.rb
new file mode 100644
index 00000000000..83393cc641d
--- /dev/null
+++ b/spec/helpers/admin/abuse_reports_helper_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::AbuseReportsHelper, feature_category: :insider_threat do
+ describe '#abuse_reports_list_data' do
+ let!(:report) { create(:abuse_report) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ let(:reports) { AbuseReport.all.page(1) }
+ let(:data) do
+ data = helper.abuse_reports_list_data(reports)[:abuse_reports_data]
+ Gitlab::Json.parse(data)
+ end
+
+ it 'has expected attributes', :aggregate_failures do
+ expect(data['pagination']).to include(
+ "current_page" => 1,
+ "per_page" => 20,
+ "total_items" => 1
+ )
+ expect(data['reports'].first).to include("category", "updated_at", "reported_user", "reporter")
+ expect(data['categories']).to match_array(AbuseReport.categories.keys)
+ end
+ end
+end
diff --git a/spec/helpers/analytics/cycle_analytics_helper_spec.rb b/spec/helpers/analytics/cycle_analytics_helper_spec.rb
deleted file mode 100644
index d906646e25c..00000000000
--- a/spec/helpers/analytics/cycle_analytics_helper_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-require "spec_helper"
-
-RSpec.describe Analytics::CycleAnalyticsHelper do
- describe '#cycle_analytics_initial_data' do
- let(:user) { create(:user, name: 'fake user', username: 'fake_user') }
- let(:image_path_keys) { [:empty_state_svg_path, :no_data_svg_path, :no_access_svg_path] }
- let(:api_path_keys) { [:milestones_path, :labels_path] }
- let(:additional_data_keys) { [:full_path, :group_id, :group_path, :project_id, :request_path] }
- let(:group) { create(:group) }
-
- subject(:cycle_analytics_data) { helper.cycle_analytics_initial_data(project, group) }
-
- before do
- project.add_maintainer(user)
- end
-
- context 'when a group is present' do
- let(:project) { create(:project, group: group) }
-
- it "sets the correct data keys" do
- expect(cycle_analytics_data.keys)
- .to match_array(api_path_keys + image_path_keys + additional_data_keys)
- end
-
- it "sets group paths" do
- expect(cycle_analytics_data)
- .to include({
- full_path: project.full_path,
- group_path: "/#{project.namespace.name}",
- group_id: project.namespace.id,
- request_path: "/#{project.full_path}/-/value_stream_analytics",
- milestones_path: "/groups/#{group.name}/-/milestones.json",
- labels_path: "/groups/#{group.name}/-/labels.json"
- })
- end
- end
-
- context 'when a group is not present' do
- let(:group) { nil }
- let(:project) { create(:project) }
-
- it "sets the correct data keys" do
- expect(cycle_analytics_data.keys)
- .to match_array(image_path_keys + api_path_keys + additional_data_keys)
- end
-
- it "sets project name space paths" do
- expect(cycle_analytics_data)
- .to include({
- full_path: project.full_path,
- group_path: project.namespace.path,
- group_id: project.namespace.id,
- request_path: "/#{project.full_path}/-/value_stream_analytics",
- milestones_path: "/#{project.full_path}/-/milestones.json",
- labels_path: "/#{project.full_path}/-/labels.json"
- })
- end
- end
- end
-end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 19cb970553b..93db4661c82 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -68,11 +68,7 @@ RSpec.describe ApplicationSettingsHelper do
))
end
- context 'when GitLab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'when on SaaS', :saas do
it 'does not contain :deactivate_dormant_users' do
expect(helper.visible_attributes).not_to include(:deactivate_dormant_users)
end
diff --git a/spec/helpers/artifacts_helper_spec.rb b/spec/helpers/artifacts_helper_spec.rb
index cf48f0ecc39..7c577cbf11c 100644
--- a/spec/helpers/artifacts_helper_spec.rb
+++ b/spec/helpers/artifacts_helper_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe ArtifactsHelper, feature_category: :build_artifacts do
it 'returns expected data' do
expect(subject).to include({
project_path: project.full_path,
+ project_id: project.id,
artifacts_management_feedback_image_path: match_asset_path('illustrations/chat-bubble-sm.svg')
})
end
diff --git a/spec/helpers/ci/catalog/resources_helper_spec.rb b/spec/helpers/ci/catalog/resources_helper_spec.rb
new file mode 100644
index 00000000000..c4abdebd12e
--- /dev/null
+++ b/spec/helpers/ci/catalog/resources_helper_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composition do
+ let_it_be(:project) { build(:project) }
+
+ describe 'can_view_private_catalog?' do
+ subject { helper.can_view_private_catalog?(project) }
+
+ before do
+ allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
+ stub_licensed_features(ci_private_catalog: false)
+ end
+
+ it 'user cannot view the Catalog in CE regardless of permissions' do
+ expect(subject).to be false
+ end
+ end
+
+ describe '#js_ci_catalog_data' do
+ let(:project) { build(:project, :repository) }
+ let(:default_helper_data) do
+ {}
+ end
+
+ subject(:catalog_data) { helper.js_ci_catalog_data }
+
+ it 'returns catalog data' do
+ expect(catalog_data).to eq(default_helper_data)
+ end
+ end
+end
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index c9aac63a883..1a0f2e10a20 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -29,12 +29,12 @@ RSpec.describe Ci::PipelineEditorHelper do
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
"ci-lint-path" => project_ci_lint_path(project),
+ "ci-troubleshooting-path" => help_page_path('ci/troubleshooting', anchor: 'common-cicd-issues'),
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'illustrations/empty.svg',
"initial-branch-name" => nil,
"includes-help-page-path" => help_page_path('ci/yaml/includes'),
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'check-cicd-syntax'),
- "lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available-message'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline-page-path" => project_pipelines_path(project),
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index 19946afb1a4..535e8f3170e 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -192,11 +192,7 @@ RSpec.describe Ci::PipelinesHelper do
end
end
- describe 'the `ios_runners_available` attribute' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ describe 'the `ios_runners_available` attribute', :saas do
subject { data[:ios_runners_available] }
context 'when the `ios_specific_templates` experiment variant is control' do
diff --git a/spec/helpers/ci/variables_helper_spec.rb b/spec/helpers/ci/variables_helper_spec.rb
index d032e7f9087..da727fd1b6b 100644
--- a/spec/helpers/ci/variables_helper_spec.rb
+++ b/spec/helpers/ci/variables_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::VariablesHelper, feature_category: :pipeline_authoring do
+RSpec.describe Ci::VariablesHelper, feature_category: :pipeline_composition do
describe '#ci_variable_maskable_raw_regex' do
it 'converts to a javascript regex' do
expect(helper.ci_variable_maskable_raw_regex).to eq("^\\S{8,}$")
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 27738f73ea5..c947d11e9de 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -325,21 +325,41 @@ RSpec.describe CommitsHelper do
assign(:path, current_path)
end
- it { is_expected.to be_an(Array) }
- it { is_expected.to include(commit) }
- it { is_expected.to include(commit.author) }
- it { is_expected.to include(ref) }
-
specify do
- is_expected.to include(
+ expect(subject).to eq([
+ commit,
+ commit.author,
+ ref,
{
merge_request: merge_request.cache_key,
pipeline_status: pipeline.cache_key,
xhr: true,
controller: "commits",
- path: current_path
+ path: current_path,
+ referenced_by: helper.tag_checksum(commit.referenced_by)
}
- )
+ ])
+ end
+
+ context 'when the show_tags_on_commits_view flag is disabled' do
+ before do
+ stub_feature_flags(show_tags_on_commits_view: false)
+ end
+
+ specify do
+ expect(subject).to eq([
+ commit,
+ commit.author,
+ ref,
+ {
+ merge_request: merge_request.cache_key,
+ pipeline_status: pipeline.cache_key,
+ xhr: true,
+ controller: "commits",
+ path: current_path
+ }
+ ])
+ end
end
describe "final cache key output" do
diff --git a/spec/helpers/device_registration_helper_spec.rb b/spec/helpers/device_registration_helper_spec.rb
new file mode 100644
index 00000000000..7556d037b3d
--- /dev/null
+++ b/spec/helpers/device_registration_helper_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe DeviceRegistrationHelper, feature_category: :system_access do
+ describe "#device_registration_data" do
+ it "returns a hash with device registration properties without initial error" do
+ device_registration_data = helper.device_registration_data(
+ current_password_required: false,
+ target_path: "/my/path",
+ webauthn_error: nil
+ )
+
+ expect(device_registration_data).to eq(
+ {
+ initial_error: nil,
+ target_path: "/my/path",
+ password_required: "false"
+ })
+ end
+
+ it "returns a hash with device registration properties with initial error" do
+ device_registration_data = helper.device_registration_data(
+ current_password_required: true,
+ target_path: "/my/path",
+ webauthn_error: { message: "my error" }
+ )
+
+ expect(device_registration_data).to eq(
+ {
+ initial_error: "my error",
+ target_path: "/my/path",
+ password_required: "true"
+ })
+ end
+ end
+end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index a46f8c13f00..2318bbf861a 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -47,6 +47,12 @@ RSpec.describe DiffHelper do
end
describe 'diff_options' do
+ let(:large_notebooks_enabled) { false }
+
+ before do
+ stub_feature_flags(large_ipynb_diffs: large_notebooks_enabled)
+ end
+
it 'returns no collapse false' do
expect(diff_options).to include(expanded: false)
end
@@ -56,21 +62,48 @@ RSpec.describe DiffHelper do
expect(diff_options).to include(expanded: true)
end
- it 'returns no collapse true if action name diff_for_path' do
- allow(controller).to receive(:action_name) { 'diff_for_path' }
- expect(diff_options).to include(expanded: true)
- end
+ context 'when action name is diff_for_path' do
+ before do
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ end
- it 'returns paths if action name diff_for_path and param old path' do
- allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
- allow(controller).to receive(:action_name) { 'diff_for_path' }
- expect(diff_options[:paths]).to include('lib/wadus.rb')
- end
+ it 'returns expanded true' do
+ expect(diff_options).to include(expanded: true)
+ end
- it 'returns paths if action name diff_for_path and param new path' do
- allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
- allow(controller).to receive(:action_name) { 'diff_for_path' }
- expect(diff_options[:paths]).to include('lib/wadus.rb')
+ it 'returns paths if param old path' do
+ allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
+ expect(diff_options[:paths]).to include('lib/wadus.rb')
+ end
+
+ it 'returns paths if param new path' do
+ allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
+ expect(diff_options[:paths]).to include('lib/wadus.rb')
+ end
+
+ it 'does not set max_patch_bytes_for_file_extension' do
+ expect(diff_options[:max_patch_bytes_for_file_extension]).to be_nil
+ end
+
+ context 'when file_identifier include .ipynb' do
+ before do
+ allow(controller).to receive(:params) { { file_identifier: 'something.ipynb' } }
+ end
+
+ context 'when large_ipynb_diffs is disabled' do
+ it 'does not set max_patch_bytes_for_file_extension' do
+ expect(diff_options[:max_patch_bytes_for_file_extension]).to be_nil
+ end
+ end
+
+ context 'when large_ipynb_diffs is enabled' do
+ let(:large_notebooks_enabled) { true }
+
+ it 'sets max_patch_bytes_for_file_extension' do
+ expect(diff_options[:max_patch_bytes_for_file_extension]).to eq({ '.ipynb' => 1.megabyte })
+ end
+ end
+ end
end
end
diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb
index 4ae1b738858..68c5289a85f 100644
--- a/spec/helpers/explore_helper_spec.rb
+++ b/spec/helpers/explore_helper_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe ExploreHelper do
describe '#explore_nav_links' do
it 'has all the expected links by default' do
- menu_items = [:projects, :groups, :snippets]
+ menu_items = [:projects, :groups, :topics, :snippets]
expect(helper.explore_nav_links).to contain_exactly(*menu_items)
end
diff --git a/spec/helpers/groups/observability_helper_spec.rb b/spec/helpers/groups/observability_helper_spec.rb
index ee33a853f9c..f0e6aa0998a 100644
--- a/spec/helpers/groups/observability_helper_spec.rb
+++ b/spec/helpers/groups/observability_helper_spec.rb
@@ -4,77 +4,46 @@ require "spec_helper"
RSpec.describe Groups::ObservabilityHelper do
let(:group) { build_stubbed(:group) }
- let(:observability_url) { Gitlab::Observability.observability_url }
describe '#observability_iframe_src' do
- context 'if observability_path is missing from params' do
- it 'returns the iframe src for action: dashboards' do
- allow(helper).to receive(:params).and_return({ action: 'dashboards' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/")
- end
-
- it 'returns the iframe src for action: manage' do
- allow(helper).to receive(:params).and_return({ action: 'manage' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/dashboards")
- end
-
- it 'returns the iframe src for action: explore' do
- allow(helper).to receive(:params).and_return({ action: 'explore' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/explore")
- end
-
- it 'returns the iframe src for action: datasources' do
- allow(helper).to receive(:params).and_return({ action: 'datasources' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/datasources")
- end
+ before do
+ allow(Gitlab::Observability).to receive(:build_full_url).and_return('full-url')
end
- context 'if observability_path exists in params' do
- context 'if observability_path is valid' do
- it 'returns the iframe src by injecting the observability path' do
- allow(helper).to receive(:params).and_return({ action: '/explore', observability_path: '/foo?bar=foobar' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/-/#{group.id}/foo?bar=foobar")
- end
- end
-
- context 'if observability_path is not valid' do
- it 'returns the iframe src by injecting the sanitised observability path' do
- allow(helper).to receive(:params).and_return({
- action: '/explore',
- observability_path:
- "/test?groupId=<script>alert('attack!')</script>"
- })
- expect(helper.observability_iframe_src(group)).to eq(
- "#{observability_url}/-/#{group.id}/test?groupId=alert('attack!')"
- )
- end
- end
+ it 'returns the iframe src for action: dashboards' do
+ allow(helper).to receive(:params).and_return({ action: 'dashboards', observability_path: '/foo?bar=foobar' })
+ expect(helper.observability_iframe_src(group)).to eq('full-url')
+ expect(Gitlab::Observability).to have_received(:build_full_url).with(group, '/foo?bar=foobar', '/')
end
- context 'when observability ui is standalone' do
- before do
- stub_env('STANDALONE_OBSERVABILITY_UI', 'true')
- end
+ it 'returns the iframe src for action: manage' do
+ allow(helper).to receive(:params).and_return({ action: 'manage', observability_path: '/foo?bar=foobar' })
+ expect(helper.observability_iframe_src(group)).to eq('full-url')
+ expect(Gitlab::Observability).to have_received(:build_full_url).with(group, '/foo?bar=foobar', '/dashboards')
+ end
- it 'returns the iframe src without group.id for action: dashboards' do
- allow(helper).to receive(:params).and_return({ action: 'dashboards' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/")
- end
+ it 'returns the iframe src for action: explore' do
+ allow(helper).to receive(:params).and_return({ action: 'explore', observability_path: '/foo?bar=foobar' })
+ expect(helper.observability_iframe_src(group)).to eq('full-url')
+ expect(Gitlab::Observability).to have_received(:build_full_url).with(group, '/foo?bar=foobar', '/explore')
+ end
- it 'returns the iframe src without group.id for action: manage' do
- allow(helper).to receive(:params).and_return({ action: 'manage' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/dashboards")
- end
+ it 'returns the iframe src for action: datasources' do
+ allow(helper).to receive(:params).and_return({ action: 'datasources', observability_path: '/foo?bar=foobar' })
+ expect(helper.observability_iframe_src(group)).to eq('full-url')
+ expect(Gitlab::Observability).to have_received(:build_full_url).with(group, '/foo?bar=foobar', '/datasources')
+ end
- it 'returns the iframe src without group.id for action: explore' do
- allow(helper).to receive(:params).and_return({ action: 'explore' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/explore")
- end
+ it 'returns the iframe src when action is not recognised' do
+ allow(helper).to receive(:params).and_return({ action: 'unrecognised', observability_path: '/foo?bar=foobar' })
+ expect(helper.observability_iframe_src(group)).to eq('full-url')
+ expect(Gitlab::Observability).to have_received(:build_full_url).with(group, '/foo?bar=foobar', '/')
+ end
- it 'returns the iframe src without group.id for action: datasources' do
- allow(helper).to receive(:params).and_return({ action: 'datasources' })
- expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/datasources")
- end
+ it 'returns the iframe src when observability_path is missing' do
+ allow(helper).to receive(:params).and_return({ action: 'dashboards' })
+ expect(helper.observability_iframe_src(group)).to eq('full-url')
+ expect(Gitlab::Observability).to have_received(:build_full_url).with(group, nil, '/')
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 8b4ac6a7cfd..ce439e5bcdd 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -436,7 +436,8 @@ RSpec.describe GroupsHelper do
it 'returns expected hash' do
expect(subgroup_creation_data(subgroup)).to eq({
import_existing_group_path: '/groups/new#import-group-pane',
- parent_group_name: name
+ parent_group_name: name,
+ parent_group_url: group_url(group)
})
end
end
@@ -445,7 +446,8 @@ RSpec.describe GroupsHelper do
it 'returns expected hash' do
expect(subgroup_creation_data(group)).to eq({
import_existing_group_path: '/groups/new#import-group-pane',
- parent_group_name: nil
+ parent_group_name: nil,
+ parent_group_url: nil
})
end
end
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index e2ee4f33eee..e5a39f6a24e 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -6,117 +6,135 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
describe '#ide_data' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.creator }
+ let_it_be(:fork_info) { { ide_path: '/test/ide/path' } }
+
+ let_it_be(:params) do
+ {
+ branch: 'master',
+ path: 'foo/bar',
+ merge_request_id: '1'
+ }
+ end
+
+ let(:base_data) do
+ {
+ 'can-use-new-web-ide' => 'false',
+ 'use-new-web-ide' => 'false',
+ 'user-preferences-path' => profile_preferences_path,
+ 'project' => nil,
+ 'preview-markdown-path' => nil
+ }
+ end
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:content_security_policy_nonce).and_return('test-csp-nonce')
end
- context 'with vscode_web_ide=true and instance vars set' do
- before do
- stub_feature_flags(vscode_web_ide: true)
- end
+ it 'returns hash' do
+ expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
+ .to include(base_data)
+ end
- it 'returns hash' do
- expect(helper.ide_data(project: project, branch: 'master', path: 'foo/README.md', merge_request: '7',
-fork_info: nil))
- .to match(
- 'can-use-new-web-ide' => 'true',
- 'use-new-web-ide' => 'true',
- 'user-preferences-path' => profile_preferences_path,
- 'new-web-ide-help-page-path' =>
- help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
- 'branch-name' => 'master',
- 'project-path' => project.path_with_namespace,
- 'csp-nonce' => 'test-csp-nonce',
- 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
- 'file-path' => 'foo/README.md',
- 'editor-font-family' => 'JetBrains Mono',
- 'editor-font-format' => 'woff2',
- 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono}),
- 'merge-request' => '7',
- 'fork-info' => nil
- )
+ context 'with project' do
+ it 'returns hash with parameters' do
+ serialized_project = API::Entities::Project.represent(project, current_user: user).to_json
+
+ expect(
+ helper.ide_data(project: project, fork_info: nil, params: params)
+ ).to include(base_data.merge(
+ 'fork-info' => nil,
+ 'branch-name' => params[:branch],
+ 'file-path' => params[:path],
+ 'merge-request' => params[:merge_request_id],
+ 'project' => serialized_project,
+ 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
+ ))
end
- it 'does not use new web ide if user.use_legacy_web_ide' do
- allow(user).to receive(:use_legacy_web_ide).and_return(true)
-
- expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
-fork_info: nil)).to include('use-new-web-ide' => 'false')
+ context 'with fork info' do
+ it 'returns hash with fork info' do
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('fork-info' => fork_info.to_json)
+ end
end
end
- context 'with vscode_web_ide=false' do
+ context 'with environments guidance experiment', :experiment do
before do
- stub_feature_flags(vscode_web_ide: false)
+ stub_experiments(in_product_guidance_environments_webide: :candidate)
end
- context 'when instance vars and parameters are not set' do
- it 'returns instance data in the hash as nil' do
- expect(helper.ide_data(project: nil, branch: nil, path: nil, merge_request: nil, fork_info: nil))
- .to include(
- 'can-use-new-web-ide' => 'false',
- 'use-new-web-ide' => 'false',
- 'user-preferences-path' => profile_preferences_path,
- 'branch-name' => nil,
- 'file-path' => nil,
- 'merge-request' => nil,
- 'fork-info' => nil,
- 'project' => nil,
- 'preview-markdown-path' => nil
- )
+ context 'when project has no enviornments' do
+ it 'enables environment guidance' do
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('enable-environments-guidance' => 'true')
end
- end
- context 'when instance vars are set' do
- it 'returns instance data in the hash' do
- fork_info = { ide_path: '/test/ide/path' }
-
- serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
-
- expect(helper.ide_data(project: project, branch: 'master', path: 'foo/bar', merge_request: '1',
-fork_info: fork_info))
- .to include(
- 'branch-name' => 'master',
- 'file-path' => 'foo/bar',
- 'merge-request' => '1',
- 'fork-info' => fork_info.to_json,
- 'project' => serialized_project,
- 'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
- )
+ context 'and the callout has been dismissed' do
+ it 'disables environment guidance' do
+ callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: user)
+ callout.update!(dismissed_at: Time.now - 1.week)
+ allow(helper).to receive(:current_user).and_return(User.find(user.id))
+
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('enable-environments-guidance' => 'false')
+ end
end
end
- context 'environments guidance experiment', :experiment do
- before do
- stub_experiments(in_product_guidance_environments_webide: :candidate)
+ context 'when the project has environments' do
+ it 'disables environment guidance' do
+ create(:environment, project: project)
+
+ expect(helper.ide_data(project: project, fork_info: fork_info, params: params))
+ .to include('enable-environments-guidance' => 'false')
end
+ end
+ end
- context 'when project has no enviornments' do
- it 'enables environment guidance' do
- expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
-fork_info: nil)).to include('enable-environments-guidance' => 'true')
- end
+ context 'with vscode_web_ide=true' do
+ let(:base_data) do
+ {
+ 'can-use-new-web-ide' => 'true',
+ 'use-new-web-ide' => 'true',
+ 'user-preferences-path' => profile_preferences_path,
+ 'new-web-ide-help-page-path' =>
+ help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
+ 'csp-nonce' => 'test-csp-nonce',
+ 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
+ 'editor-font-family' => 'JetBrains Mono',
+ 'editor-font-format' => 'woff2',
+ 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono})
+ }
+ end
- context 'and the callout has been dismissed' do
- it 'disables environment guidance' do
- callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
- callout.update!(dismissed_at: Time.now - 1.week)
- allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
- expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
-fork_info: nil)).to include('enable-environments-guidance' => 'false')
- end
- end
- end
+ before do
+ stub_feature_flags(vscode_web_ide: true)
+ end
- context 'when the project has environments' do
- it 'disables environment guidance' do
- create(:environment, project: project)
+ it 'returns hash' do
+ expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
+ .to include(base_data)
+ end
- expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
-fork_info: nil)).to include('enable-environments-guidance' => 'false')
- end
+ it 'does not use new web ide if user.use_legacy_web_ide' do
+ allow(user).to receive(:use_legacy_web_ide).and_return(true)
+
+ expect(helper.ide_data(project: nil, fork_info: fork_info, params: params))
+ .to include('use-new-web-ide' => 'false')
+ end
+
+ context 'with project' do
+ it 'returns hash with parameters' do
+ expect(
+ helper.ide_data(project: project, fork_info: nil, params: params)
+ ).to include(base_data.merge(
+ 'branch-name' => params[:branch],
+ 'file-path' => params[:path],
+ 'merge-request' => params[:merge_request_id],
+ 'fork-info' => nil
+ ))
end
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 1ae834c0769..fd10b204e50 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -293,10 +293,13 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
end
describe '#issuable_reference' do
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project, project_namespace: project_namespace) }
+
context 'when show_full_reference truthy' do
it 'display issuable full reference' do
assign(:show_full_reference, true)
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_reference(issue)).to eql(issue.to_reference(full: true))
end
@@ -305,12 +308,10 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
context 'when show_full_reference falsey' do
context 'when @group present' do
it 'display issuable reference to @group' do
- project = build_stubbed(:project)
-
assign(:show_full_reference, nil)
assign(:group, project.namespace)
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project.namespace))
end
@@ -318,13 +319,11 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
context 'when @project present' do
it 'display issuable reference to @project' do
- project = build_stubbed(:project)
-
assign(:show_full_reference, nil)
assign(:group, nil)
assign(:project, project)
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project))
end
@@ -333,8 +332,11 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
end
describe '#issuable_project_reference' do
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project, project_namespace: project_namespace) }
+
it 'display project name and simple reference with `#` to an issue' do
- issue = build_stubbed(:issue)
+ issue = build_stubbed(:issue, project: project)
expect(helper.issuable_project_reference(issue)).to eq("#{issue.project.full_name} ##{issue.iid}")
end
@@ -430,7 +432,8 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
action: "show",
namespace_id: "foo",
project_id: "bar",
- id: incident.iid
+ id: incident.iid,
+ incident_tab: 'timeline'
}).permit!
end
@@ -441,7 +444,9 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do
expected_data = {
issueType: 'incident',
hasLinkedAlerts: false,
- canUpdateTimelineEvent: true
+ canUpdateTimelineEvent: true,
+ currentPath: "/foo/bar/-/issues/incident/#{incident.iid}/timeline",
+ currentTab: 'timeline'
}
expect(helper.issuable_initial_data(incident)).to match(hash_including(expected_data))
diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb
index 31aeff85c70..4f56bb7467f 100644
--- a/spec/helpers/jira_connect_helper_spec.rb
+++ b/spec/helpers/jira_connect_helper_spec.rb
@@ -9,8 +9,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
let(:user) { create(:user) }
let(:client_id) { '123' }
- let(:enable_public_keys_storage_config) { false }
- let(:enable_public_keys_storage_setting) { false }
+ let(:enable_public_keys_storage) { false }
before do
stub_application_setting(jira_connect_application_key: client_id)
@@ -22,9 +21,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
before do
allow(view).to receive(:current_user).and_return(nil)
allow(Gitlab.config.gitlab).to receive(:url).and_return('http://test.host')
- allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
- .and_return(enable_public_keys_storage_config)
- stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage_setting)
+ stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage)
end
it 'includes Jira Connect app attributes' do
@@ -108,16 +105,8 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
expect(subject[:public_key_storage_enabled]).to eq(false)
end
- context 'when public_key_storage is enabled via config' do
- let(:enable_public_keys_storage_config) { true }
-
- it 'assignes public_key_storage_enabled to true' do
- expect(subject[:public_key_storage_enabled]).to eq(true)
- end
- end
-
- context 'when public_key_storage is enabled via setting' do
- let(:enable_public_keys_storage_setting) { true }
+ context 'when public_key_storage is enabled' do
+ let(:enable_public_keys_storage) { true }
it 'assignes public_key_storage_enabled to true' do
expect(subject[:public_key_storage_enabled]).to eq(true)
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 088519248c6..a9f99f29f6d 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -534,6 +534,45 @@ RSpec.describe MarkupHelper do
helper.first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)
end.not_to change { Gitlab::GitalyClient.get_request_count }
end
+
+ it 'strips non-user links' do
+ html = 'This a cool [website](https://gitlab.com/).'
+
+ object = create_object(html)
+ result = helper.first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)
+
+ expect(result).to include('This a cool website.')
+ end
+
+ it 'styles the current user link', :aggregate_failures do
+ another_user = create(:user)
+ html = "Please have a look, @#{user.username} @#{another_user.username}!"
+
+ object = create_object(html)
+ result = helper.first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)
+ links = Nokogiri::HTML.parse(result).css('//a')
+
+ expect(links[0].classes).to include('current-user')
+ expect(links[1].classes).not_to include('current-user')
+ end
+
+ context 'when current_user is nil' do
+ before do
+ allow(helper).to receive(:current_user).and_return(nil)
+ end
+
+ it 'renders the link with no styling when current_user is nil' do
+ another_user = create(:user)
+ html = "Please have a look, @#{user.username} @#{another_user.username}!"
+
+ object = create_object(html)
+ result = helper.first_line_in_markdown(object, attribute, 100, is_todo: true, project: project)
+ links = Nokogiri::HTML.parse(result).css('//a')
+
+ expect(links[0].classes).not_to include('current-user')
+ expect(links[1].classes).not_to include('current-user')
+ end
+ end
end
context 'when the asked attribute can be redacted' do
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 3a66fe474ab..5ae057dc97d 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -11,8 +11,11 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
let(:with_can_create_project) { false }
let(:with_can_create_group) { false }
let(:with_can_create_snippet) { false }
+ let(:title) { 'Create new...' }
- subject(:view_model) { helper.new_dropdown_view_model(project: current_project, group: current_group) }
+ subject(:view_model) do
+ helper.new_dropdown_view_model(project: current_project, group: current_group)
+ end
before do
allow(helper).to receive(:current_user) { current_user }
@@ -22,7 +25,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
allow(user).to receive(:can?).with(:create_snippet) { with_can_create_snippet }
end
- shared_examples 'invite member item' do
+ shared_examples 'invite member item' do |partial|
it 'shows invite member link with emoji' do
expect(view_model[:menu_sections]).to eq(
expected_menu_section(
@@ -30,12 +33,12 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'invite',
title: 'Invite members',
- emoji: 'shaking_hands',
- href: expected_href,
+ icon: 'shaking_hands',
+ partial: partial,
+ component: 'invite_members',
data: {
- track_action: 'click_link_invite_members',
- track_label: 'plus_menu_dropdown',
- track_property: 'navigation_top'
+ trigger_source: 'top-nav',
+ trigger_element: 'text-emoji'
}
)
)
@@ -54,8 +57,13 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
end
context 'when group and project are nil' do
- it 'has no menu sections' do
- expect(view_model[:menu_sections]).to eq([])
+ it 'has base results' do
+ results = {
+ title: title,
+ menu_sections: []
+ }
+
+ expect(view_model).to eq(results)
end
context 'when can create project' do
@@ -145,8 +153,13 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
.to receive(:can?).with(current_user, :admin_group_member, group) { with_can_admin_in_group }
end
- it 'has no menu sections' do
- expect(view_model[:menu_sections]).to eq([])
+ it 'has base results' do
+ results = {
+ title: title,
+ menu_sections: []
+ }
+
+ expect(view_model).to eq(results)
end
context 'when can create projects in group' do
@@ -199,7 +212,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
let(:expected_title) { 'In this group' }
let(:expected_href) { "/groups/#{group.full_path}/-/group_members" }
- it_behaves_like 'invite member item'
+ it_behaves_like 'invite member item', 'groups/invite_members_top_nav_link'
end
end
@@ -219,8 +232,13 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
allow(helper).to receive(:can_admin_project_member?) { with_can_admin_project_member }
end
- it 'has no menu sections' do
- expect(view_model[:menu_sections]).to eq([])
+ it 'has base results' do
+ results = {
+ title: title,
+ menu_sections: []
+ }
+
+ expect(view_model).to eq(results)
end
context 'with show_new_issue_link?' do
@@ -296,7 +314,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
let(:expected_title) { 'In this project' }
let(:expected_href) { "/#{project.path_with_namespace}/-/project_members" }
- it_behaves_like 'invite member item'
+ it_behaves_like 'invite member item', 'projects/invite_members_top_nav_link'
end
end
@@ -311,22 +329,27 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
allow(helper).to receive(:can?).with(current_user, :create_projects, group).and_return(true)
end
- it 'gives precedence to group over project' do
- group_section = expected_menu_section(
- title: 'In this group',
+ it 'gives precedence to project over group' do
+ project_section = expected_menu_section(
+ title: 'In this project',
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
- id: 'new_project',
- title: 'New project/repository',
- href: "/projects/new?namespace_id=#{group.id}",
+ id: 'new_issue',
+ title: 'New issue',
+ href: "/#{project.path_with_namespace}/-/issues/new",
data: {
- track_action: 'click_link_new_project_group',
+ track_action: 'click_link_new_issue',
track_label: 'plus_menu_dropdown',
- track_property: 'navigation_top'
+ track_property: 'navigation_top',
+ qa_selector: 'new_issue_link'
}
)
)
+ results = {
+ title: title,
+ menu_sections: project_section
+ }
- expect(view_model[:menu_sections]).to eq(group_section)
+ expect(view_model).to eq(results)
end
end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index ce5ac2e5404..2b69557b840 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -56,6 +56,7 @@ RSpec.describe Nav::TopNavHelper do
expected_primary = [
{ href: '/explore', icon: 'project', id: 'project', title: 'Projects' },
{ href: '/explore/groups', icon: 'group', id: 'groups', title: 'Groups' },
+ { href: '/explore/projects/topics', icon: 'labels', id: 'topics', title: 'Topics' },
{ href: '/explore/snippets', icon: 'snippet', id: 'snippets', title: 'Snippets' }
].map do |item|
::Gitlab::Nav::TopNavMenuItem.build(**item)
@@ -79,6 +80,12 @@ RSpec.describe Nav::TopNavHelper do
css_class: 'dashboard-shortcuts-groups'
},
{
+ href: '/explore/projects/topics',
+ id: 'topics-shortcut',
+ title: 'Topics',
+ css_class: 'dashboard-shortcuts-topics'
+ },
+ {
href: '/explore/snippets',
id: 'snippets-shortcut',
title: 'Snippets',
@@ -320,20 +327,6 @@ RSpec.describe Nav::TopNavHelper do
context 'with milestones' 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: { **menu_data_tracking_attrs('milestones') },
- href: '/dashboard/milestones',
- icon: 'clock',
- id: 'milestones',
- title: 'Milestones'
- )
- expect(subject[:primary]).to eq([expected_header, expected_primary])
- end
-
it 'has expected :shortcuts' do
expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
id: 'milestones-shortcut',
@@ -348,23 +341,6 @@ RSpec.describe Nav::TopNavHelper do
context 'with snippets' 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',
- **menu_data_tracking_attrs('snippets')
- },
- href: '/dashboard/snippets',
- icon: 'snippet',
- id: 'snippets',
- title: 'Snippets'
- )
- expect(subject[:primary]).to eq([expected_header, expected_primary])
- end
-
it 'has expected :shortcuts' do
expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
id: 'snippets-shortcut',
@@ -379,20 +355,6 @@ RSpec.describe Nav::TopNavHelper do
context 'with activity' 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: { **menu_data_tracking_attrs('activity') },
- href: '/dashboard/activity',
- icon: 'history',
- id: 'activity',
- title: 'Activity'
- )
- expect(subject[:primary]).to eq([expected_header, expected_primary])
- end
-
it 'has expected :shortcuts' do
expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
id: 'activity-shortcut',
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 68a6b6293c8..91635ffcdc0 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe NotesHelper do
+RSpec.describe NotesHelper, feature_category: :team_planning do
include RepoHelpers
let_it_be(:owner) { create(:owner) }
@@ -223,6 +223,17 @@ RSpec.describe NotesHelper do
end
end
+ describe '#initial_notes_data' do
+ it 'return initial notes data for issuable' do
+ autocomplete = '/autocomplete/users'
+ @project = project
+ @noteable = create(:issue, project: @project)
+
+ expect(helper.initial_notes_data(autocomplete).keys).to match_array(%i[notesUrl now diffView enableGFM])
+ expect(helper.initial_notes_data(autocomplete)[:enableGFM].keys).to match(%i[emojis members issues mergeRequests vulnerabilities epics milestones labels])
+ end
+ end
+
describe '#notes_url' do
it 'return snippet notes path for personal snippet' do
@snippet = create(:personal_snippet)
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index fc69aee4e04..b6546a2eaf3 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PackagesHelper do
+RSpec.describe PackagesHelper, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
@@ -38,11 +38,18 @@ RSpec.describe PackagesHelper do
describe '#pypi_registry_url' do
let_it_be(:base_url_with_token) { base_url.sub('://', '://__token__:<your_personal_token>@') }
+ let_it_be(:public_project) { create(:project, :public) }
- it 'returns the pypi registry url' do
- url = helper.pypi_registry_url(1)
+ it 'returns the pypi registry url with token when project is private' do
+ url = helper.pypi_registry_url(project)
- expect(url).to eq("#{base_url_with_token}projects/1/packages/pypi/simple")
+ expect(url).to eq("#{base_url_with_token}projects/#{project.id}/packages/pypi/simple")
+ end
+
+ it 'returns the pypi registry url without token when project is public' do
+ url = helper.pypi_registry_url(public_project)
+
+ expect(url).to eq("#{base_url}projects/#{public_project.id}/packages/pypi/simple")
end
end
diff --git a/spec/helpers/plan_limits_helper_spec.rb b/spec/helpers/plan_limits_helper_spec.rb
new file mode 100644
index 00000000000..121338c9cf2
--- /dev/null
+++ b/spec/helpers/plan_limits_helper_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PlanLimitsHelper, feature_category: :continuous_integration do
+ describe '#plan_limit_setting_description' do
+ it 'describes known limits', :aggregate_failures do
+ [
+ :ci_pipeline_size,
+ :ci_active_jobs,
+ :ci_active_pipelines,
+ :ci_project_subscriptions,
+ :ci_pipeline_schedules,
+ :ci_needs_size_limit,
+ :ci_registered_group_runners,
+ :ci_registered_project_runners,
+ :pipeline_hierarchy_size
+ ].each do |limit_name|
+ expect(helper.plan_limit_setting_description(limit_name)).to be_present
+ end
+ end
+
+ it 'raises an ArgumentError on invalid arguments' do
+ expect { helper.plan_limit_setting_description(:some_invalid_limit) }.to(
+ raise_error(ArgumentError, /No description/)
+ )
+ end
+ end
+end
diff --git a/spec/helpers/projects/settings/branch_rules_helper_spec.rb b/spec/helpers/projects/settings/branch_rules_helper_spec.rb
new file mode 100644
index 00000000000..35a21f72f11
--- /dev/null
+++ b/spec/helpers/projects/settings/branch_rules_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Settings::BranchRulesHelper, feature_category: :source_code_management do
+ let_it_be(:project) { build_stubbed(:project) }
+
+ describe '#branch_rules_data' do
+ subject(:data) { helper.branch_rules_data(project) }
+
+ it 'returns branch rules data' do
+ expect(data).to match({
+ project_path: project.full_path,
+ protected_branches_path: project_settings_repository_path(project, anchor: 'js-protected-branches-settings'),
+ approval_rules_path: project_settings_merge_requests_path(project,
+ anchor: 'js-merge-request-approval-settings'),
+ status_checks_path: project_settings_merge_requests_path(project, anchor: 'js-merge-request-settings'),
+ branches_path: project_branches_path(project),
+ show_status_checks: 'false',
+ show_approvers: 'false',
+ show_code_owners: 'false'
+ })
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 477b5cd7753..93352715ff4 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -1286,7 +1286,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management 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 February 2023. 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}. Contact GitLab Support if you have any additional questions.'))
end
end
@@ -1373,9 +1373,36 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
source_name: source_project.full_name,
source_path: project_path(source_project),
ahead_compare_path: ahead_path,
- behind_compare_path: behind_path
+ behind_compare_path: behind_path,
+ source_default_branch: source_project.default_branch
})
end
end
end
+
+ describe '#remote_mirror_setting_enabled?' do
+ it 'returns false' do
+ expect(helper.remote_mirror_setting_enabled?).to be_falsy
+ end
+ end
+
+ describe '#http_clone_url_to_repo' do
+ before do
+ allow(project).to receive(:http_url_to_repo).and_return('http_url_to_repo')
+ end
+
+ subject { helper.http_clone_url_to_repo(project) }
+
+ it { expect(subject).to eq('http_url_to_repo') }
+ end
+
+ describe '#ssh_clone_url_to_repo' do
+ before do
+ allow(project).to receive(:ssh_url_to_repo).and_return('ssh_url_to_repo')
+ end
+
+ subject { helper.ssh_clone_url_to_repo(project) }
+
+ it { expect(subject).to eq('ssh_url_to_repo') }
+ end
end
diff --git a/spec/helpers/registrations_helper_spec.rb b/spec/helpers/registrations_helper_spec.rb
index eec87bc8712..b2f9a794cb3 100644
--- a/spec/helpers/registrations_helper_spec.rb
+++ b/spec/helpers/registrations_helper_spec.rb
@@ -8,20 +8,4 @@ RSpec.describe RegistrationsHelper do
expect(helper.signup_username_data_attributes.keys).to include(:min_length, :min_length_message, :max_length, :max_length_message, :qa_selector)
end
end
-
- describe '#arkose_labs_challenge_enabled?' do
- before do
- stub_application_setting(
- arkose_labs_private_api_key: nil,
- arkose_labs_public_api_key: nil,
- arkose_labs_namespace: nil
- )
- stub_env('ARKOSE_LABS_PRIVATE_KEY', nil)
- stub_env('ARKOSE_LABS_PUBLIC_KEY', nil)
- end
-
- it 'is false' do
- expect(helper.arkose_labs_challenge_enabled?).to eq false
- end
- end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index c7afe0bf391..ba703914049 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -829,6 +829,21 @@ RSpec.describe SearchHelper, feature_category: :global_search do
expect(header_search_context[:project_metadata]).to eq(project_metadata)
end
+ context 'feature issues is not available' do
+ let(:feature_available) { false }
+ let(:project_metadata) { { mr_path: project_merge_requests_path(project) } }
+
+ before do
+ allow(project).to receive(:feature_available?).and_call_original
+ allow(project).to receive(:feature_available?).with(:issues, current_user).and_return(feature_available)
+ end
+
+ it 'adds the :project and :project-metadata correctly to hash' do
+ expect(header_search_context[:project]).to eq({ id: project.id, name: project.name })
+ expect(header_search_context[:project_metadata]).to eq(project_metadata)
+ end
+ end
+
context 'with scope' do
let(:scope) { 'issues' }
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 672c2ef7589..dbb6f9bd9f3 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -62,36 +62,71 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
end
describe '#super_sidebar_context' do
- let(:user) { build(:user) }
- let(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
+ let_it_be(:group) { build(:group) }
+ let_it_be(:panel) { {} }
- subject { helper.super_sidebar_context(user, group: group, project: nil) }
+ subject do
+ helper.super_sidebar_context(user, group: group, project: nil, panel: panel)
+ end
before do
allow(helper).to receive(:current_user) { user }
- Rails.cache.write(['users', user.id, 'assigned_open_issues_count'], 1)
- Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 4)
- Rails.cache.write(['users', user.id, 'review_requested_open_merge_requests_count'], 0)
- Rails.cache.write(['users', user.id, 'todos_pending_count'], 3)
- Rails.cache.write(['users', user.id, 'total_merge_requests_count'], 4)
+ allow(helper).to receive(:can?).and_return(true)
+ allow(panel).to receive(:super_sidebar_menu_items).and_return(nil)
+ allow(panel).to receive(:super_sidebar_context_header).and_return(nil)
+ allow(user).to receive(:assigned_open_issues_count).and_return(1)
+ allow(user).to receive(:assigned_open_merge_requests_count).and_return(4)
+ allow(user).to receive(:review_requested_open_merge_requests_count).and_return(0)
+ allow(user).to receive(:todos_pending_count).and_return(3)
+ allow(user).to receive(:total_merge_requests_count).and_return(4)
end
it 'returns sidebar values from user', :use_clean_rails_memory_store_caching do
expect(subject).to include({
+ current_context_header: nil,
+ current_menu_items: nil,
name: user.name,
username: user.username,
avatar_url: user.avatar_url,
+ has_link_to_profile: helper.current_user_menu?(:profile),
+ link_to_profile: user_url(user),
+ status: {
+ can_update: helper.can?(user, :update_user_status, user),
+ busy: user.status&.busy?,
+ customized: user.status&.customized?,
+ availability: user.status&.availability.to_s,
+ emoji: user.status&.emoji,
+ message: user.status&.message_html&.html_safe,
+ clear_after: nil
+ },
+ trial: {
+ has_start_trial: helper.current_user_menu?(:start_trial),
+ url: helper.trials_link_url
+ },
+ settings: {
+ has_settings: helper.current_user_menu?(:settings),
+ profile_path: profile_path,
+ profile_preferences_path: profile_preferences_path
+ },
+ can_sign_out: helper.current_user_menu?(:sign_out),
+ sign_out_link: destroy_user_session_path,
assigned_open_issues_count: 1,
todos_pending_count: 3,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
total_merge_requests_count: 4,
+ projects_path: projects_path,
+ groups_path: groups_path,
support_path: helper.support_url,
display_whats_new: helper.display_whats_new?,
whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count,
whats_new_version_digest: helper.whats_new_version_digest,
show_version_check: helper.show_version_check?,
gitlab_version: Gitlab.version_info,
- gitlab_version_check: helper.gitlab_version_check
+ gitlab_version_check: helper.gitlab_version_check,
+ gitlab_com_but_not_canary: Gitlab.com_but_not_canary?,
+ gitlab_com_and_canary: Gitlab.com_and_canary?,
+ canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url
})
end
@@ -138,7 +173,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
items: array_including(
{ href: "/projects/new", text: "New project/repository" },
{ href: "/groups/new#create-group-pane", text: "New subgroup" },
- { href: "/groups/#{group.full_path}/-/group_members", text: "Invite members" }
+ { href: '', text: "Invite members" }
)
),
a_hash_including(
@@ -151,5 +186,108 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
)
)
end
+
+ describe 'current context' do
+ context 'when current context is a project' do
+ let_it_be(:project) { build(:project) }
+
+ subject do
+ helper.super_sidebar_context(user, group: nil, project: project, panel: panel)
+ end
+
+ before do
+ allow(project).to receive(:persisted?).and_return(true)
+ end
+
+ it 'returns project context' do
+ expect(subject[:current_context]).to eq({
+ namespace: 'projects',
+ item: {
+ id: project.id,
+ avatarUrl: project.avatar_url,
+ name: project.name,
+ namespace: project.full_name,
+ webUrl: project_path(project)
+ }
+ })
+ end
+ end
+
+ context 'when current context is a group' do
+ subject do
+ helper.super_sidebar_context(user, group: group, project: nil, panel: panel)
+ end
+
+ before do
+ allow(group).to receive(:persisted?).and_return(true)
+ end
+
+ it 'returns group context' do
+ expect(subject[:current_context]).to eq({
+ namespace: 'groups',
+ item: {
+ id: group.id,
+ avatarUrl: group.avatar_url,
+ name: group.name,
+ namespace: group.full_name,
+ webUrl: group_path(group)
+ }
+ })
+ end
+ end
+
+ context 'when current context is not tracked' do
+ subject do
+ helper.super_sidebar_context(user, group: nil, project: nil, panel: panel)
+ end
+
+ it 'returns no context' do
+ expect(subject[:current_context]).to eq({})
+ end
+ end
+ end
+ end
+
+ describe '#super_sidebar_nav_panel' do
+ let(:user) { build(:user) }
+ let(:group) { build(:group) }
+ let(:project) { build(:project) }
+
+ before do
+ allow(helper).to receive(:project_sidebar_context_data).and_return(
+ { current_user: nil, container: project, can_view_pipeline_editor: false, learn_gitlab_enabled: false })
+ allow(helper).to receive(:group_sidebar_context_data).and_return({ current_user: nil, container: group })
+
+ allow(group).to receive(:to_global_id).and_return(5)
+ Rails.cache.write(['users', user.id, 'assigned_open_issues_count'], 1)
+ Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 4)
+ Rails.cache.write(['users', user.id, 'review_requested_open_merge_requests_count'], 0)
+ Rails.cache.write(['users', user.id, 'todos_pending_count'], 3)
+ Rails.cache.write(['users', user.id, 'total_merge_requests_count'], 4)
+ end
+
+ it 'returns Project Panel for project nav' do
+ expect(helper.super_sidebar_nav_panel(nav: 'project')).to be_a(Sidebars::Projects::SuperSidebarPanel)
+ end
+
+ it 'returns Group Panel for group nav' do
+ expect(helper.super_sidebar_nav_panel(nav: 'group')).to be_a(Sidebars::Groups::SuperSidebarPanel)
+ end
+
+ it 'returns User Settings Panel for profile nav' do
+ expect(helper.super_sidebar_nav_panel(nav: 'profile')).to be_a(Sidebars::UserSettings::Panel)
+ end
+
+ it 'returns User profile Panel for user profile nav' do
+ expect(helper.super_sidebar_nav_panel(nav: 'user_profile')).to be_a(Sidebars::UserProfile::Panel)
+ end
+
+ it 'returns "Your Work" Panel for your_work nav', :use_clean_rails_memory_store_caching do
+ expect(helper.super_sidebar_nav_panel(nav: 'your_work', user: user)).to be_a(Sidebars::YourWork::Panel)
+ end
+
+ it 'returns "Your Work" Panel as a fallback', :use_clean_rails_memory_store_caching do
+ expect(helper.super_sidebar_nav_panel(user: user)).to be_a(Sidebars::YourWork::Panel)
+ end
end
end
diff --git a/spec/helpers/sorting_helper_spec.rb b/spec/helpers/sorting_helper_spec.rb
index d561b08efac..d625b46e286 100644
--- a/spec/helpers/sorting_helper_spec.rb
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -10,6 +10,60 @@ RSpec.describe SortingHelper do
allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: { label_name: option }))
end
+ describe '#issuable_sort_options' do
+ let(:viewing_issues) { false }
+ let(:viewing_merge_requests) { false }
+ let(:params) { {} }
+
+ subject(:options) { helper.issuable_sort_options(viewing_issues, viewing_merge_requests) }
+
+ before do
+ allow(helper).to receive(:params).and_return(params)
+ end
+
+ shared_examples 'with merged date option' do
+ it 'adds merged date option' do
+ expect(options).to include(
+ a_hash_including(
+ value: 'merged_at',
+ text: 'Merged date'
+ )
+ )
+ end
+ end
+
+ shared_examples 'without merged date option' do
+ it 'does not set merged date option' do
+ expect(options).not_to include(
+ a_hash_including(
+ value: 'merged_at',
+ text: 'Merged date'
+ )
+ )
+ end
+ end
+
+ it_behaves_like 'without merged date option'
+
+ context 'when viewing_merge_requests is true' do
+ let(:viewing_merge_requests) { true }
+
+ it_behaves_like 'without merged date option'
+
+ context 'when state param is all' do
+ let(:params) { { state: 'all' } }
+
+ it_behaves_like 'with merged date option'
+ end
+
+ context 'when state param is merged' do
+ let(:params) { { state: 'merged' } }
+
+ it_behaves_like 'with merged date option'
+ end
+ end
+ end
+
describe '#admin_users_sort_options' do
it 'returns correct link attributes in array' do
options = admin_users_sort_options(filter: 'filter', search_query: 'search')
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 26951b0c1e7..8d24e9576e0 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -135,18 +135,6 @@ RSpec.describe TodosHelper do
context 'when given a task' do
let(:todo) { task_todo }
- context 'when the use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'responds with an appropriate path' do
- path = helper.todo_target_path(todo)
-
- expect(path).to eq("/#{todo.project.full_path}/-/work_items/#{todo.target.id}")
- end
- end
-
it 'responds with an appropriate path using iid' do
path = helper.todo_target_path(todo)
diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index 4cb179e4f60..cb724816daf 100644
--- a/spec/helpers/users/callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -165,7 +165,27 @@ RSpec.describe Users::CalloutsHelper do
end
end
- describe '#web_hook_disabled_dismissed?' do
+ describe '.show_pages_menu_callout?' do
+ subject { helper.show_pages_menu_callout? }
+
+ before do
+ allow(helper).to receive(:user_dismissed?).with(described_class::PAGES_MOVED_CALLOUT) { dismissed }
+ end
+
+ context 'when user has not dismissed' do
+ let(:dismissed) { false }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user dismissed' do
+ let(:dismissed) { true }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#web_hook_disabled_dismissed?', feature_category: :integrations do
context 'without a project' do
it 'is false' do
expect(helper).not_to be_web_hook_disabled_dismissed(nil)
@@ -174,50 +194,12 @@ RSpec.describe Users::CalloutsHelper do
context 'with a project' do
let_it_be(:project) { create(:project) }
+ let(:factory) { :project_callout }
+ let(:container_key) { :project }
+ let(:container) { project }
+ let(:key) { "web_hooks:last_failure:project-#{project.id}" }
- context 'the web-hook failure callout has never been dismissed' do
- it 'is false' do
- expect(helper).not_to be_web_hook_disabled_dismissed(project)
- end
- end
-
- context 'the web-hook failure callout has been dismissed', :freeze_time do
- before do
- create(:project_callout,
- feature_name: described_class::WEB_HOOK_DISABLED,
- user: user,
- project: project,
- dismissed_at: 1.week.ago)
- end
-
- it 'is true' do
- expect(helper).to be_web_hook_disabled_dismissed(project)
- end
-
- context 'when there was an older failure', :clean_gitlab_redis_shared_state do
- let(:key) { "web_hooks:last_failure:project-#{project.id}" }
-
- before do
- Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) }
- end
-
- it 'is true' do
- expect(helper).to be_web_hook_disabled_dismissed(project)
- end
- end
-
- context 'when there has been a more recent failure', :clean_gitlab_redis_shared_state do
- let(:key) { "web_hooks:last_failure:project-#{project.id}" }
-
- before do
- Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) }
- end
-
- it 'is false' do
- expect(helper).not_to be_web_hook_disabled_dismissed(project)
- end
- end
- end
+ include_examples 'CalloutsHelper#web_hook_disabled_dismissed shared examples'
end
end
end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index c2c78be6a0f..2829236f7d1 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe UsersHelper do
include TermsHelper
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user, timezone: ActiveSupport::TimeZone::MAPPING['UTC']) }
def filter_ee_badges(badges)
badges.reject { |badge| badge[:text] == 'Is using seat' }
@@ -37,6 +37,35 @@ RSpec.describe UsersHelper do
end
end
+ describe '#user_clear_status_at' do
+ context 'when status exists' do
+ context 'with clear_status_at set' do
+ it 'has the correct iso formatted date', time_travel_to: '2020-01-01 00:00:00 +0000' do
+ clear_status_at = 1.day.from_now
+ status = build_stubbed(:user_status, clear_status_at: clear_status_at)
+
+ expect(user_clear_status_at(status.user)).to eq('2020-01-02T00:00:00Z')
+ end
+ end
+
+ context 'without clear_status_at set' do
+ it 'returns nil' do
+ status = build_stubbed(:user_status, clear_status_at: nil)
+
+ expect(user_clear_status_at(status.user)).to be_nil
+ end
+ end
+ end
+
+ context 'without status' do
+ it 'returns nil' do
+ user = build_stubbed(:user)
+
+ expect(user_clear_status_at(user)).to be_nil
+ end
+ end
+ end
+
describe '#profile_tabs' do
subject(:tabs) { helper.profile_tabs }
@@ -468,4 +497,31 @@ RSpec.describe UsersHelper do
expect(data[:paths]).to match_schema('entities/admin_users_data_attributes_paths')
end
end
+
+ describe '#trials_link_url' do
+ it 'returns the correct URL' do
+ if Gitlab.ee?
+ expect(trials_link_url).to eq('/-/trial_registrations/new?glm_content=top-right-dropdown&glm_source=gitlab.com')
+ else
+ expect(trials_link_url).to eq('https://about.gitlab.com/free-trial/')
+ end
+ end
+ end
+
+ describe '#user_profile_tabs_app_data' do
+ before do
+ allow(helper).to receive(:user_calendar_path).with(user, :json).and_return('/users/root/calendar.json')
+ allow(user).to receive_message_chain(:followers, :count).and_return(2)
+ allow(user).to receive_message_chain(:followees, :count).and_return(3)
+ end
+
+ it 'returns expected hash' do
+ expect(helper.user_profile_tabs_app_data(user)).to eq({
+ followees: 3,
+ followers: 2,
+ user_calendar_path: '/users/root/calendar.json',
+ utc_offset: 0
+ })
+ end
+ end
end
diff --git a/spec/initializers/check_forced_decomposition_spec.rb b/spec/initializers/check_forced_decomposition_spec.rb
index a216f078932..64cb1184e7a 100644
--- a/spec/initializers/check_forced_decomposition_spec.rb
+++ b/spec/initializers/check_forced_decomposition_spec.rb
@@ -95,11 +95,7 @@ RSpec.describe 'check_forced_decomposition initializer', feature_category: :pods
it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
- context 'for GitLab.com' do
- before do
- allow(::Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'for SaaS', :saas do
it { expect { check_forced_decomposition }.not_to raise_error }
end
diff --git a/spec/initializers/direct_upload_support_spec.rb b/spec/initializers/direct_upload_support_spec.rb
index 670deecb4f1..68dd12fdb6e 100644
--- a/spec/initializers/direct_upload_support_spec.rb
+++ b/spec/initializers/direct_upload_support_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe 'Direct upload support' do
end
context 'when other provider is used' do
- let(:provider) { 'Rackspace' }
+ let(:provider) { 'Aliyun' }
it 'raises an error' do
expect { subject }.to raise_error /Object storage provider '#{provider}' is not supported when 'direct_upload' is used for '#{config_name}'/
@@ -103,7 +103,7 @@ RSpec.describe 'Direct upload support' do
context 'when object storage is disabled' do
let(:enabled) { false }
let(:direct_upload) { false }
- let(:provider) { 'Rackspace' }
+ let(:provider) { 'Aliyun' }
it 'succeeds' do
expect { subject }.not_to raise_error
diff --git a/spec/initializers/google_cloud_profiler_spec.rb b/spec/initializers/google_cloud_profiler_spec.rb
new file mode 100644
index 00000000000..493d1e0bea5
--- /dev/null
+++ b/spec/initializers/google_cloud_profiler_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'google cloud profiler', :aggregate_failures, feature_category: :application_performance do
+ subject(:load_initializer) do
+ load rails_root_join('config/initializers/google_cloud_profiler.rb')
+ end
+
+ shared_examples 'does not call profiler agent' do
+ it do
+ expect(CloudProfilerAgent::Agent).not_to receive(:new)
+
+ load_initializer
+ end
+ end
+
+ context 'when GITLAB_GOOGLE_CLOUD_PROFILER_ENABLED is set to true' do
+ before do
+ stub_env('GITLAB_GOOGLE_CLOUD_PROFILER_ENABLED', true)
+ end
+
+ context 'when GITLAB_GOOGLE_CLOUD_PROFILER_PROJECT_ID is not set' do
+ include_examples 'does not call profiler agent'
+ end
+
+ context 'when GITLAB_GOOGLE_CLOUD_PROFILER_PROJECT_ID is set' do
+ let(:project_id) { 'gitlab-staging-1' }
+ let(:agent) { instance_double(CloudProfilerAgent::Agent) }
+
+ before do
+ stub_env('GITLAB_GOOGLE_CLOUD_PROFILER_PROJECT_ID', project_id)
+ end
+
+ context 'when run in Puma context' do
+ before do
+ allow(::Gitlab::Runtime).to receive(:puma?).and_return(true)
+ allow(::Gitlab::Runtime).to receive(:sidekiq?).and_return(false)
+ end
+
+ it 'calls the agent' do
+ expect(CloudProfilerAgent::Agent)
+ .to receive(:new).with(service: 'gitlab-web', project_id: project_id,
+ logger: an_instance_of(::Gitlab::AppJsonLogger),
+ log_labels: hash_including(
+ message: 'Google Cloud Profiler Ruby',
+ pid: be_a(Integer),
+ worker_id: be_a(String)
+ )).and_return(agent)
+ expect(agent).to receive(:start)
+
+ load_initializer
+ end
+ end
+
+ context 'when run in Sidekiq context' do
+ before do
+ allow(::Gitlab::Runtime).to receive(:puma?).and_return(false)
+ allow(::Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+ end
+
+ include_examples 'does not call profiler agent'
+ end
+
+ context 'when run in another context' do
+ before do
+ allow(::Gitlab::Runtime).to receive(:puma?).and_return(false)
+ allow(::Gitlab::Runtime).to receive(:sidekiq?).and_return(false)
+ end
+
+ include_examples 'does not call profiler agent'
+ end
+ end
+ end
+
+ context 'when GITLAB_GOOGLE_CLOUD_PROFILER_ENABLED is not set' do
+ include_examples 'does not call profiler agent'
+ end
+
+ context 'when GITLAB_GOOGLE_CLOUD_PROFILER_ENABLED is set to false' do
+ before do
+ stub_env('GITLAB_GOOGLE_CLOUD_PROFILER_ENABLED', false)
+ end
+
+ include_examples 'does not call profiler agent'
+ end
+end
diff --git a/spec/initializers/safe_session_store_patch_spec.rb b/spec/initializers/safe_session_store_patch_spec.rb
new file mode 100644
index 00000000000..b48aae02e9a
--- /dev/null
+++ b/spec/initializers/safe_session_store_patch_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'safe_sesion_store_patch', feature_category: :integrations do
+ shared_examples 'safe session store' do
+ it 'allows storing a String' do
+ session[:good_data] = 'hello world'
+
+ expect(session[:good_data]).to eq('hello world')
+ end
+
+ it 'raises error when session attempts to store an unsafe object' do
+ expect { session[:test] = Struct.new(:test) }
+ .to raise_error(/Serializing novel Ruby objects can cause uninitialized constants in mixed deployments/)
+ end
+
+ it 'allows instance double of OneLogin::RubySaml::Response' do
+ response_double = instance_double(OneLogin::RubySaml::Response)
+
+ session[:response_double] = response_double
+
+ expect(session[:response_double]).to eq(response_double)
+ end
+
+ it 'raises an error for instance double of REXML::Document' do
+ response_double = instance_double(REXML::Document)
+
+ expect { session[:response_double] = response_double }
+ .to raise_error(/Serializing novel Ruby objects can cause uninitialized constants in mixed deployments/)
+ end
+ end
+
+ context 'with ActionController::TestSession' do
+ let(:session) { ActionController::TestSession.new }
+
+ it_behaves_like 'safe session store'
+ end
+
+ context 'with ActionDispatch::Request::Session' do
+ let(:dummy_store) do
+ Class.new do
+ def load_session(_env)
+ [1, {}]
+ end
+
+ def session_exists?(_env)
+ true
+ end
+
+ def delete_session(_env, _id, _options)
+ 123
+ end
+ end.new
+ end
+
+ let(:request) { ActionDispatch::Request.new({}) }
+ let(:session) { ActionDispatch::Request::Session.create(dummy_store, request, {}) }
+
+ it_behaves_like 'safe session store'
+ end
+end
diff --git a/spec/lib/api/entities/ssh_key_spec.rb b/spec/lib/api/entities/ssh_key_spec.rb
index b4310035a66..14561beedc5 100644
--- a/spec/lib/api/entities/ssh_key_spec.rb
+++ b/spec/lib/api/entities/ssh_key_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Entities::SSHKey, feature_category: :authentication_and_authorization do
+RSpec.describe API::Entities::SSHKey, feature_category: :system_access do
describe '#as_json' do
subject { entity.as_json }
diff --git a/spec/lib/api/helpers/internal_helpers_spec.rb b/spec/lib/api/helpers/internal_helpers_spec.rb
new file mode 100644
index 00000000000..847b711f829
--- /dev/null
+++ b/spec/lib/api/helpers/internal_helpers_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe API::Helpers::InternalHelpers, feature_category: :api do
+ describe "log user git operation activity" do
+ let_it_be(:project) { create(:project) }
+ let(:user) { project.first_owner }
+ let(:internal_helper) do
+ Class.new { include API::Helpers::InternalHelpers }.new
+ end
+
+ before do
+ allow(internal_helper).to receive(:project).and_return(project)
+ end
+
+ shared_examples "handles log git operation activity" do
+ it "log the user activity" do
+ activity_service = instance_double(::Users::ActivityService)
+
+ args = { author: user, project: project, namespace: project&.namespace }
+
+ expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service)
+ expect(activity_service).to receive(:execute)
+
+ internal_helper.log_user_activity(user)
+ end
+ end
+
+ context "when git pull/fetch/clone action" do
+ before do
+ allow(internal_helper).to receive(:params).and_return(action: "git-upload-pack")
+ end
+
+ context "with log the user activity" do
+ it_behaves_like "handles log git operation activity"
+ end
+ end
+
+ context "when git push action" do
+ before do
+ allow(internal_helper).to receive(:params).and_return(action: "git-receive-pack")
+ end
+
+ it "does not log the user activity when log_user_git_push_activity is disabled" do
+ stub_feature_flags(log_user_git_push_activity: false)
+
+ expect(::Users::ActivityService).not_to receive(:new)
+
+ internal_helper.log_user_activity(user)
+ end
+
+ context "with log the user activity when log_user_git_push_activity is enabled" do
+ stub_feature_flags(log_user_git_push_activity: true)
+
+ it_behaves_like "handles log git operation activity"
+ end
+ end
+ end
+end
diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb
index 2a663d5e9b2..6ba4396c396 100644
--- a/spec/lib/api/helpers/packages_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages_helpers_spec.rb
@@ -306,7 +306,8 @@ RSpec.describe API::Helpers::PackagesHelpers, feature_category: :package_registr
label: label,
namespace: namespace,
property: property,
- project: project
+ project: project,
+ user: user
)
end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 0fcf36ca9dd..b70bcb5ab0d 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Helpers, feature_category: :not_owned do
+RSpec.describe API::Helpers, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
subject(:helper) { Class.new.include(described_class).new }
diff --git a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
index 48787f2a0d2..f05adb49651 100644
--- a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
+++ b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb
@@ -29,11 +29,11 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity, feature_categor
end
context 'when the pipeline does belong to a Jira issue' do
- let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
+ let(:pipeline) { create(:ci_pipeline, merge_request: merge_request, project: project) }
%i[jira_branch jira_title jira_description].each do |trait|
context "because it belongs to an MR with a #{trait}" do
- let(:merge_request) { create(:merge_request, trait) }
+ let(:merge_request) { create(:merge_request, trait, source_project: project) }
describe '#issue_keys' do
it 'is not empty' do
@@ -48,5 +48,22 @@ RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity, feature_categor
end
end
end
+
+ context 'in the pipeline\'s commit messsage' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:commit_message) { "Merge branch 'staging' into 'master'\n\nFixes bug described in PROJ-1234" }
+
+ before do
+ allow(pipeline).to receive(:git_commit_message).and_return(commit_message)
+ end
+
+ describe '#issue_keys' do
+ it { expect(subject.issue_keys).to match_array(['PROJ-1234']) }
+ end
+
+ describe '#to_json' do
+ it { expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.build_info) }
+ end
+ end
end
end
diff --git a/spec/lib/atlassian/jira_connect_spec.rb b/spec/lib/atlassian/jira_connect_spec.rb
index 14bf13b8fe6..5238fbdb7cd 100644
--- a/spec/lib/atlassian/jira_connect_spec.rb
+++ b/spec/lib/atlassian/jira_connect_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Atlassian::JiraConnect, feature_category: :integrations do
describe '.app_name' do
diff --git a/spec/lib/atlassian/jira_issue_key_extractor_spec.rb b/spec/lib/atlassian/jira_issue_key_extractor_spec.rb
index ce29e03f818..42fc441b868 100644
--- a/spec/lib/atlassian/jira_issue_key_extractor_spec.rb
+++ b/spec/lib/atlassian/jira_issue_key_extractor_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Atlassian::JiraIssueKeyExtractor do
+RSpec.describe Atlassian::JiraIssueKeyExtractor, feature_category: :integrations do
describe '.has_keys?' do
subject { described_class.has_keys?(string) }
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
index 7cc8ce2cbae..ad0e5553fa1 100644
--- a/spec/lib/backup/gitaly_backup_spec.rb
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -17,7 +17,8 @@ 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,
+ 'GITALY_SERVERS' => anything
}.merge(ENV)
end
@@ -125,12 +126,18 @@ RSpec.describe Backup::GitalyBackup do
}
end
+ let(:expected_env) do
+ ssl_env.merge(
+ 'GITALY_SERVERS' => anything
+ )
+ end
+
before do
stub_const('ENV', ssl_env)
end
it 'passes through SSL envs' do
- expect(Open3).to receive(:popen2).with(ssl_env, anything, 'create', '-path', anything, '-layout', 'pointer', '-id', backup_id).and_call_original
+ expect(Open3).to receive(:popen2).with(expected_env, anything, 'create', '-path', anything, '-layout', 'pointer', '-id', backup_id).and_call_original
subject.start(:create, destination, backup_id: backup_id)
subject.finish!
diff --git a/spec/lib/banzai/filter/inline_observability_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_filter_spec.rb
index fb1ba46e76c..81896faced8 100644
--- a/spec/lib/banzai/filter/inline_observability_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_observability_filter_spec.rb
@@ -2,25 +2,20 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::InlineObservabilityFilter do
+RSpec.describe Banzai::Filter::InlineObservabilityFilter, feature_category: :metrics do
include FilterSpecHelper
let(:input) { %(<a href="#{url}">example</a>) }
let(:doc) { filter(input) }
- let(:group) { create(:group) }
- let(:user) { create(:user) }
- describe '#filter?' do
- context 'when the document has an external link' do
- let(:url) { 'https://foo.com' }
-
- it 'leaves regular non-observability links unchanged' do
- expect(doc.to_s).to eq(input)
- end
- end
+ before do
+ allow(Gitlab::Observability).to receive(:embeddable_url).and_return('embeddable-url')
+ stub_config_setting(url: "https://www.gitlab.com")
+ end
- context 'when the document contains an embeddable observability link' do
- let(:url) { 'https://observe.gitlab.com/12345' }
+ describe '#filter?' do
+ context 'when the document contains a valid observability link' do
+ let(:url) { "https://www.gitlab.com/groups/some-group/-/observability/test" }
it 'leaves the original link unchanged' do
expect(doc.at_css('a').to_s).to eq(input)
@@ -30,17 +25,34 @@ RSpec.describe Banzai::Filter::InlineObservabilityFilter do
node = doc.at_css('.js-render-observability')
expect(node).to be_present
- expect(node.attribute('data-frame-url').to_s).to eq(url)
+ expect(node.attribute('data-frame-url').to_s).to eq('embeddable-url')
+ expect(Gitlab::Observability).to have_received(:embeddable_url).with(url).once
end
end
- context 'when feature flag is disabled' do
- let(:url) { 'https://observe.gitlab.com/12345' }
+ context 'with duplicate URLs' do
+ let(:url) { "https://www.gitlab.com/groups/some-group/-/observability/test" }
+ let(:input) { %(<a href="#{url}">example1</a><a href="#{url}">example2</a>) }
- before do
- stub_feature_flags(observability_group_tab: false)
+ where(:embeddable_url) do
+ [
+ 'not-nil',
+ nil
+ ]
end
+ with_them do
+ it 'calls Gitlab::Observability.embeddable_url only once' do
+ allow(Gitlab::Observability).to receive(:embeddable_url).with(url).and_return(embeddable_url)
+
+ filter(input)
+
+ expect(Gitlab::Observability).to have_received(:embeddable_url).with(url).once
+ end
+ end
+ end
+
+ shared_examples 'does not embed observabilty' do
it 'leaves the original link unchanged' do
expect(doc.at_css('a').to_s).to eq(input)
end
@@ -51,5 +63,39 @@ RSpec.describe Banzai::Filter::InlineObservabilityFilter do
expect(node).not_to be_present
end
end
+
+ context 'when the embeddable url is nil' do
+ let(:url) { "https://www.gitlab.com/groups/some-group/-/something-else/test" }
+
+ before do
+ allow(Gitlab::Observability).to receive(:embeddable_url).and_return(nil)
+ end
+
+ it_behaves_like 'does not embed observabilty'
+ end
+
+ context 'when the document has an unrecognised link' do
+ let(:url) { "https://www.gitlab.com/groups/some-group/-/something-else/test" }
+
+ it_behaves_like 'does not embed observabilty'
+
+ it 'does not build the embeddable url' do
+ expect(Gitlab::Observability).not_to have_received(:embeddable_url)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ let(:url) { "https://www.gitlab.com/groups/some-group/-/observability/test" }
+
+ before do
+ stub_feature_flags(observability_group_tab: false)
+ end
+
+ it_behaves_like 'does not embed observabilty'
+
+ it 'does not build the embeddable url' do
+ expect(Gitlab::Observability).not_to have_received(:embeddable_url)
+ end
+ end
end
end
diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
index 1fdb29b688e..80061539a0b 100644
--- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
@@ -162,6 +162,54 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference} - closed)")
end
+
+ it 'shows title for references with +s' do
+ issue = create_issue(:opened, title: 'Some issue')
+ link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+s')
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference}) • Unassigned")
+ end
+
+ context 'when extended summary props are present' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:assignees) { create_list(:user, 3) }
+ let_it_be(:issue) { create_issue(:opened, title: 'Some issue', milestone: milestone, assignees: assignees) }
+ let_it_be(:link) do
+ create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+s')
+ end
+
+ it 'shows extended summary for references with +s' do
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq(
+ "#{issue.title} (#{issue.to_reference}) • #{assignees[0].name}, #{assignees[1].name}+ • #{milestone.title}"
+ )
+ end
+
+ describe 'checking N+1' do
+ let_it_be(:milestone2) { create(:milestone, project: project) }
+ let_it_be(:assignees2) { create_list(:user, 3) }
+
+ it 'does not have N+1 for extended summary', :use_sql_query_cache do
+ issue2 = create_issue(:opened, title: 'Another issue', milestone: milestone2, assignees: assignees2)
+ link2 = create_link(issue2.to_reference, issue: issue2.id, reference_type: 'issue', reference_format: '+s')
+
+ # warm up
+ filter(link, context)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ filter(link, context)
+ end.count
+
+ expect(control_count).to eq 10
+
+ expect do
+ filter("#{link} #{link2}", context)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+ end
end
context 'for merge request references' do
@@ -235,5 +283,80 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor
expect(doc.css('a').last.text).to eq("#{merge_request.title} (#{merge_request.to_reference})")
end
+
+ it 'shows title for references with +s' do
+ merge_request = create_merge_request(:opened, title: 'Some merge request')
+
+ link = create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request',
+ reference_format: '+s'
+ )
+
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq("#{merge_request.title} (#{merge_request.to_reference}) • Unassigned")
+ end
+
+ context 'when extended summary props are present' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:assignees) { create_list(:user, 2) }
+ let_it_be(:merge_request) do
+ create_merge_request(:opened, title: 'Some merge request', milestone: milestone, assignees: assignees)
+ end
+
+ let_it_be(:link) do
+ create_link(
+ merge_request.to_reference,
+ merge_request: merge_request.id,
+ reference_type: 'merge_request',
+ reference_format: '+s'
+ )
+ end
+
+ it 'shows extended summary for references with +s' do
+ doc = filter(link, context)
+
+ expect(doc.css('a').last.text).to eq(
+ "#{merge_request.title} (#{merge_request.to_reference}) • #{assignees[0].name}, #{assignees[1].name} • " \
+ "#{milestone.title}"
+ )
+ end
+
+ describe 'checking N+1' do
+ let_it_be(:milestone2) { create(:milestone, project: project) }
+ let_it_be(:assignees2) { create_list(:user, 3) }
+
+ it 'does not have N+1 for extended summary', :use_sql_query_cache do
+ merge_request2 = create_merge_request(
+ :closed,
+ title: 'Some merge request',
+ milestone: milestone2,
+ assignees: assignees2
+ )
+
+ link2 = create_link(
+ merge_request2.to_reference,
+ merge_request: merge_request2.id,
+ reference_type: 'merge_request',
+ reference_format: '+s'
+ )
+
+ # warm up
+ filter(link, context)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ filter(link, context)
+ end.count
+
+ expect(control_count).to eq 10
+
+ expect do
+ filter("#{link} #{link2}", context)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index d8a97c6c3dc..aadd726ac40 100644
--- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -150,6 +150,15 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor
expect(link.attr('href')).to eq(issue_url)
end
+ it 'includes a data-reference-format attribute for extended summary URL references' do
+ doc = reference_filter("Issue #{issue_url}+s")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-reference-format')
+ expect(link.attr('data-reference-format')).to eq('+s')
+ expect(link.attr('href')).to eq(issue_url)
+ end
+
it 'supports an :only_path context' do
doc = reference_filter("Issue #{written_reference}", only_path: true)
link = doc.css('a').first.attr('href')
diff --git a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
index 9853d6f4093..156455221cf 100644
--- a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
@@ -128,6 +128,15 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter, feature_
expect(link.attr('href')).to eq(merge_request_url)
end
+ it 'includes a data-reference-format attribute for extended summary URL references' do
+ doc = reference_filter("Merge #{merge_request_url}+s")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-reference-format')
+ expect(link.attr('data-reference-format')).to eq('+s')
+ expect(link.attr('href')).to eq(merge_request_url)
+ end
+
it 'supports an :only_path context' do
doc = reference_filter("Merge #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
index 7307daca516..7e5ca00f118 100644
--- a/spec/lib/banzai/filter/references/reference_cache_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -76,8 +76,7 @@ RSpec.describe Banzai::Filter::References::ReferenceCache, feature_category: :te
cache_single.load_records_per_parent
end.count
- expect(control_count).to eq 1
-
+ expect(control_count).to eq 2
# Since this is an issue filter that is not batching issue queries
# across projects, we have to account for that.
# 1 for original issue, 2 for second route/project, 1 for other issue
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb
index 780f61f8c61..40261947750 100644
--- a/spec/lib/bulk_imports/clients/http_spec.rb
+++ b/spec/lib/bulk_imports/clients/http_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe BulkImports::Clients::HTTP, feature_category: :importers do
let(:resource) { 'resource' }
let(:version) { "#{BulkImport::MIN_MAJOR_VERSION}.0.0" }
let(:enterprise) { false }
+ let(:sidekiq_request_timeout) { described_class::SIDEKIQ_REQUEST_TIMEOUT }
let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
let(:metadata_response) do
double(
@@ -123,6 +124,36 @@ RSpec.describe BulkImports::Clients::HTTP, feature_category: :importers do
allow(Gitlab::HTTP).to receive(:get).with(uri, params).and_return(response)
end
end
+
+ context 'when the request is asynchronous' do
+ let(:expected_args) do
+ [
+ 'http://gitlab.example/api/v4/resource',
+ hash_including(
+ query: {
+ page: described_class::DEFAULT_PAGE,
+ per_page: described_class::DEFAULT_PER_PAGE,
+ private_token: token
+ },
+ headers: {
+ 'Content-Type' => 'application/json'
+ },
+ follow_redirects: true,
+ resend_on_redirect: false,
+ limit: 2,
+ timeout: sidekiq_request_timeout
+ )
+ ]
+ end
+
+ it 'sets a timeout that is double the default read timeout' do
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+
+ expect(Gitlab::HTTP).to receive(method).with(*expected_args).and_return(response_double)
+
+ subject.public_send(method, resource)
+ end
+ end
end
describe '#post' do
@@ -253,7 +284,7 @@ RSpec.describe BulkImports::Clients::HTTP, feature_category: :importers do
stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
.to_return(status: 404, body: "", headers: { 'Content-Type' => 'application/json' })
- expect { subject.instance_version }.to raise_exception(BulkImports::Error, 'Import aborted as it was not possible to connect to the provided GitLab instance URL.')
+ expect { subject.instance_version }.to raise_exception(BulkImports::Error, 'Invalid source URL. Enter only the base URL of the source GitLab instance.')
end
end
diff --git a/spec/lib/bulk_imports/features_spec.rb b/spec/lib/bulk_imports/features_spec.rb
deleted file mode 100644
index a92e4706bbe..00000000000
--- a/spec/lib/bulk_imports/features_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Features do
- describe '.project_migration_enabled' do
- let_it_be(:top_level_namespace) { create(:group) }
-
- context 'when bulk_import_projects feature flag is enabled' do
- it 'returns true' do
- stub_feature_flags(bulk_import_projects: true)
-
- expect(described_class.project_migration_enabled?).to eq(true)
- end
-
- context 'when feature flag is enabled on root ancestor level' do
- it 'returns true' do
- stub_feature_flags(bulk_import_projects: top_level_namespace)
-
- expect(described_class.project_migration_enabled?(top_level_namespace.full_path)).to eq(true)
- end
- end
-
- context 'when feature flag is enabled on a different top level namespace' do
- it 'returns false' do
- stub_feature_flags(bulk_import_projects: top_level_namespace)
-
- different_namepace = create(:group)
-
- expect(described_class.project_migration_enabled?(different_namepace.full_path)).to eq(false)
- end
- end
- end
-
- context 'when bulk_import_projects feature flag is disabled' do
- it 'returns false' do
- stub_feature_flags(bulk_import_projects: false)
-
- expect(described_class.project_migration_enabled?(top_level_namespace.full_path)).to eq(false)
- end
- end
- end
-end
diff --git a/spec/lib/bulk_imports/groups/stage_spec.rb b/spec/lib/bulk_imports/groups/stage_spec.rb
index cc772f07d21..7c3127beb97 100644
--- a/spec/lib/bulk_imports/groups/stage_spec.rb
+++ b/spec/lib/bulk_imports/groups/stage_spec.rb
@@ -68,40 +68,16 @@ RSpec.describe BulkImports::Groups::Stage, feature_category: :importers do
end
end
- context 'when bulk_import_projects feature flag is enabled' do
- it 'includes project entities pipeline' do
- stub_feature_flags(bulk_import_projects: true)
-
- expect(described_class.new(entity).pipelines).to include(
- hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
- )
- end
-
- describe 'migrate projects flag' do
- context 'when true' do
- it 'includes project entities pipeline' do
- entity.update!(migrate_projects: true)
-
- expect(described_class.new(entity).pipelines).to include(
- hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
- )
- end
- end
-
- context 'when false' do
- it 'does not include project entities pipeline' do
- entity.update!(migrate_projects: false)
-
- expect(described_class.new(entity).pipelines).not_to include(
- hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
- )
- end
- end
- end
+ it 'includes project entities pipeline' do
+ expect(described_class.new(entity).pipelines).to include(
+ hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
+ )
+ end
- context 'when feature flag is enabled on root ancestor level' do
+ describe 'migrate projects flag' do
+ context 'when true' do
it 'includes project entities pipeline' do
- stub_feature_flags(bulk_import_projects: ancestor)
+ entity.update!(migrate_projects: true)
expect(described_class.new(entity).pipelines).to include(
hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
@@ -109,24 +85,22 @@ RSpec.describe BulkImports::Groups::Stage, feature_category: :importers do
end
end
- context 'when destination namespace is not present' do
- it 'includes project entities pipeline' do
- stub_feature_flags(bulk_import_projects: true)
-
- entity = create(:bulk_import_entity, destination_namespace: '')
+ context 'when false' do
+ it 'does not include project entities pipeline' do
+ entity.update!(migrate_projects: false)
- expect(described_class.new(entity).pipelines).to include(
+ expect(described_class.new(entity).pipelines).not_to include(
hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
)
end
end
end
- context 'when bulk_import_projects feature flag is disabled' do
- it 'does not include project entities pipeline' do
- stub_feature_flags(bulk_import_projects: false)
+ context 'when destination namespace is not present' do
+ it 'includes project entities pipeline' do
+ entity = create(:bulk_import_entity, destination_namespace: '')
- expect(described_class.new(entity).pipelines).not_to include(
+ expect(described_class.new(entity).pipelines).to include(
hash_including({ pipeline: BulkImports::Groups::Pipelines::ProjectEntitiesPipeline })
)
end
diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
index 25edc9feea8..29f42ab3366 100644
--- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::NdjsonPipeline do
+RSpec.describe BulkImports::NdjsonPipeline, feature_category: :importers do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@@ -150,13 +150,63 @@ RSpec.describe BulkImports::NdjsonPipeline do
describe '#load' do
context 'when object is not persisted' do
+ it 'saves the object using RelationObjectSaver' do
+ object = double(persisted?: false, new_record?: true)
+
+ allow(subject).to receive(:relation_definition)
+
+ expect_next_instance_of(Gitlab::ImportExport::Base::RelationObjectSaver) do |saver|
+ expect(saver).to receive(:execute)
+ end
+
+ subject.load(nil, object)
+ end
+
+ context 'when object is invalid' do
+ it 'captures invalid subrelations' do
+ entity = create(:bulk_import_entity, group: group)
+ tracker = create(:bulk_import_tracker, entity: entity)
+ context = BulkImports::Pipeline::Context.new(tracker)
+
+ allow(subject).to receive(:context).and_return(context)
+
+ object = group.labels.new(priorities: [LabelPriority.new])
+ object.validate
+
+ allow_next_instance_of(Gitlab::ImportExport::Base::RelationObjectSaver) do |saver|
+ allow(saver).to receive(:execute)
+ allow(saver).to receive(:invalid_subrelations).and_return(object.priorities)
+ end
+
+ subject.load(context, object)
+
+ failure = entity.failures.first
+
+ expect(failure.pipeline_class).to eq(tracker.pipeline_name)
+ expect(failure.exception_class).to eq('RecordInvalid')
+ expect(failure.exception_message).to eq("Project can't be blank, Priority can't be blank, and Priority is not a number")
+ end
+ end
+ end
+
+ context 'when object is persisted' do
it 'saves the object' do
- object = double(persisted?: false)
+ object = double(new_record?: false, invalid?: false)
expect(object).to receive(:save!)
subject.load(nil, object)
end
+
+ context 'when object is invalid' do
+ it 'raises ActiveRecord::RecordInvalid exception' do
+ object = build_stubbed(:issue)
+
+ expect(Gitlab::Import::Errors).to receive(:merge_nested_errors).with(object)
+
+ expect { subject.load(nil, object) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
end
context 'when object is missing' do
diff --git a/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb
index a78f524b227..63e7cdf2e5a 100644
--- a/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb
@@ -109,6 +109,13 @@ RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline do
'name' => 'first status',
'status' => 'created'
}
+ ],
+ 'builds' => [
+ {
+ 'name' => 'second status',
+ 'status' => 'created',
+ 'ref' => 'abcd'
+ }
]
}
]
@@ -119,6 +126,7 @@ RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline do
stage = project.all_pipelines.first.stages.first
expect(stage.name).to eq('test stage')
expect(stage.statuses.first.name).to eq('first status')
+ expect(stage.builds.first.name).to eq('second status')
end
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/commit_notes_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/commit_notes_pipeline_spec.rb
new file mode 100644
index 00000000000..f5f31c83033
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/pipelines/commit_notes_pipeline_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Pipelines::CommitNotesPipeline, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_slug: 'destination-project',
+ destination_namespace: group.full_path
+ )
+ end
+
+ let(:ci_pipeline_note) do
+ {
+ "note" => "Commit note 1",
+ "noteable_type" => "Commit",
+ "author_id" => 1,
+ "created_at" => "2023-01-30T19:27:36.585Z",
+ "updated_at" => "2023-02-10T14:43:01.308Z",
+ "project_id" => 1,
+ "commit_id" => "sha-notes",
+ "system" => false,
+ "updated_by_id" => 1,
+ "discussion_id" => "e3fde7d585c6467a7a5147e83617eb6daa61aaf4",
+ "last_edited_at" => "2023-02-10T14:43:01.306Z",
+ "author" => {
+ "name" => "Administrator"
+ },
+ "events" => [
+ {
+ "project_id" => 1,
+ "author_id" => 1,
+ "action" => "commented",
+ "target_type" => "Note"
+ }
+ ]
+ }
+ end
+
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ subject(:pipeline) { described_class.new(context) }
+
+ describe '#run' do
+ before do
+ group.add_owner(user)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
+ allow(extractor).to receive(:extract).and_return(
+ BulkImports::Pipeline::ExtractedData.new(data: [ci_pipeline_note])
+ )
+ end
+ end
+
+ it 'imports ci pipeline notes into destination project' do
+ expect { pipeline.run }.to change { project.notes.for_commit_id("sha-notes").count }.from(0).to(1)
+ end
+ end
+end
diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb
index 7d78aad8b13..73364ec9698 100644
--- a/spec/lib/container_registry/gitlab_api_client_spec.rb
+++ b/spec/lib/container_registry/gitlab_api_client_spec.rb
@@ -320,6 +320,98 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
end
end
+ describe '#sub_repositories_with_tag' do
+ let(:path) { 'namespace/path/to/repository' }
+ let(:page_size) { 100 }
+ let(:last) { nil }
+ let(:response) do
+ [
+ {
+ "name": "docker-alpine",
+ "path": "gitlab-org/build/cng/docker-alpine",
+ "created_at": "2022-06-07T12:11:13.633+00:00",
+ "updated_at": "2022-06-07T14:37:49.251+00:00"
+ },
+ {
+ "name": "git-base",
+ "path": "gitlab-org/build/cng/git-base",
+ "created_at": "2022-06-07T12:11:13.633+00:00",
+ "updated_at": "2022-06-07T14:37:49.251+00:00"
+ }
+ ]
+ end
+
+ let(:result_with_no_pagination) do
+ {
+ pagination: {},
+ response_body: ::Gitlab::Json.parse(response.to_json)
+ }
+ end
+
+ subject { client.sub_repositories_with_tag(path, page_size: page_size, last: last) }
+
+ context 'with valid parameters' do
+ before do
+ stub_sub_repositories_with_tag(path, page_size: page_size, respond_with: response)
+ end
+
+ it { is_expected.to eq(result_with_no_pagination) }
+ end
+
+ context 'with a response with a link header' do
+ let(:next_page_url) { 'http://sandbox.org/test?last=c' }
+ let(:expected) do
+ {
+ pagination: { next: { uri: URI(next_page_url) } },
+ response_body: ::Gitlab::Json.parse(response.to_json)
+ }
+ end
+
+ before do
+ stub_sub_repositories_with_tag(path, page_size: page_size, next_page_url: next_page_url, respond_with: response)
+ end
+
+ it { is_expected.to eq(expected) }
+ end
+
+ context 'with a large page size set' do
+ let(:page_size) { described_class::MAX_TAGS_PAGE_SIZE + 1000 }
+
+ before do
+ stub_sub_repositories_with_tag(path, page_size: described_class::MAX_TAGS_PAGE_SIZE, respond_with: response)
+ end
+
+ it { is_expected.to eq(result_with_no_pagination) }
+ end
+
+ context 'with a last parameter set' do
+ let(:last) { 'test' }
+
+ before do
+ stub_sub_repositories_with_tag(path, page_size: page_size, last: last, respond_with: response)
+ end
+
+ it { is_expected.to eq(result_with_no_pagination) }
+ end
+
+ context 'with non successful response' do
+ before do
+ stub_sub_repositories_with_tag(path, page_size: page_size, status_code: 404)
+ end
+
+ it 'logs an error and returns an empty hash' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:log_exception).with(
+ instance_of(described_class::UnsuccessfulResponseError),
+ class: described_class.name,
+ url: "/gitlab/v1/repository-paths/#{path}/repositories/list/",
+ status_code: 404
+ )
+ expect(subject).to eq({})
+ end
+ end
+ end
+
describe '.supports_gitlab_api?' do
subject { described_class.supports_gitlab_api? }
@@ -439,6 +531,90 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
end
end
+ describe '.one_project_with_container_registry_tag' do
+ let(:path) { 'build/cng/docker-alpine' }
+ let(:response_body) do
+ [
+ {
+ "name" => "docker-alpine",
+ "path" => path,
+ "created_at" => "2022-06-07T12:11:13.633+00:00",
+ "updated_at" => "2022-06-07T14:37:49.251+00:00"
+ }
+ ]
+ end
+
+ let(:response) do
+ {
+ pagination: { next: { uri: URI('http://sandbox.org/test?last=x') } },
+ response_body: ::Gitlab::Json.parse(response_body.to_json)
+ }
+ end
+
+ let_it_be(:group) { create(:group, path: 'build') }
+ let_it_be(:project) { create(:project, name: 'cng', namespace: group) }
+ let_it_be(:container_repository) { create(:container_repository, project: project, name: "docker-alpine") }
+
+ shared_examples 'fetching the project from container repository and path' do
+ it 'fetches the project from the given path details' do
+ expect(ContainerRegistry::Path).to receive(:new).with(path).and_call_original
+ expect(ContainerRepository).to receive(:find_by_path).and_call_original
+
+ expect(subject).to eq(project)
+ end
+
+ it 'returns nil when path is invalid' do
+ registry_path = ContainerRegistry::Path.new('invalid')
+ expect(ContainerRegistry::Path).to receive(:new).with(path).and_return(registry_path)
+ expect(registry_path.valid?).to eq(false)
+
+ expect(subject).to eq(nil)
+ end
+
+ it 'returns nil when there is no container_repository matching the path' do
+ expect(ContainerRegistry::Path).to receive(:new).with(path).and_call_original
+ expect(ContainerRepository).to receive(:find_by_path).and_return(nil)
+
+ expect(subject).to eq(nil)
+ end
+ end
+
+ subject { described_class.one_project_with_container_registry_tag(path) }
+
+ before do
+ expect(Auth::ContainerRegistryAuthenticationService).to receive(:pull_nested_repositories_access_token).with(path.downcase).and_return(token)
+ stub_container_registry_config(enabled: true, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
+ end
+
+ context 'with successful response' do
+ before do
+ stub_sub_repositories_with_tag(path, page_size: 1, respond_with: response_body)
+ end
+
+ it_behaves_like 'fetching the project from container repository and path'
+ end
+
+ context 'with unsuccessful response' do
+ before do
+ stub_sub_repositories_with_tag(path, page_size: 1, status_code: 404, respond_with: {})
+ end
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'with uppercase path' do
+ let(:path) { 'BuilD/CNG/docker-alpine' }
+
+ before do
+ expect_next_instance_of(described_class) do |client|
+ expect(client).to receive(:sub_repositories_with_tag).with(path.downcase, page_size: 1).and_return(response.with_indifferent_access).once
+ end
+ end
+
+ it_behaves_like 'fetching the project from container repository and path'
+ end
+ end
+
def stub_pre_import(path, status_code, pre:)
import_type = pre ? 'pre' : 'final'
stub_request(:put, "#{registry_api_url}/gitlab/v1/import/#{path}/?import_type=#{import_type}")
@@ -525,4 +701,30 @@ RSpec.describe ContainerRegistry::GitlabApiClient do
headers: response_headers
)
end
+
+ def stub_sub_repositories_with_tag(path, page_size: nil, last: nil, next_page_url: nil, status_code: 200, respond_with: {})
+ params = { n: page_size, last: last }.compact
+
+ url = "#{registry_api_url}/gitlab/v1/repository-paths/#{path}/repositories/list/"
+
+ if params.present?
+ url += "?#{params.map { |param, val| "#{param}=#{val}" }.join('&')}"
+ end
+
+ request_headers = { 'Accept' => described_class::JSON_TYPE }
+ request_headers['Authorization'] = "bearer #{token}" if token
+
+ response_headers = { 'Content-Type' => described_class::JSON_TYPE }
+ if next_page_url
+ response_headers['Link'] = "<#{next_page_url}>; rel=\"next\""
+ end
+
+ stub_request(:get, url)
+ .with(headers: request_headers)
+ .to_return(
+ status: status_code,
+ body: respond_with.to_json,
+ headers: response_headers
+ )
+ end
end
diff --git a/spec/lib/error_tracking/collector/payload_validator_spec.rb b/spec/lib/error_tracking/collector/payload_validator_spec.rb
index 94708f63bf4..96ad66e9b58 100644
--- a/spec/lib/error_tracking/collector/payload_validator_spec.rb
+++ b/spec/lib/error_tracking/collector/payload_validator_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ErrorTracking::Collector::PayloadValidator do
end
with_them do
- let(:payload) { Gitlab::Json.parse(fixture_file(event_fixture)) }
+ let(:payload) { Gitlab::Json.parse(File.read(event_fixture)) }
it_behaves_like 'valid payload'
end
diff --git a/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
new file mode 100644
index 00000000000..d533bcf0039
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/batched_background_migration_generator_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rails/generators/testing/behaviour'
+require 'rails/generators/testing/assertions'
+
+RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator, feature_category: :database do
+ include Rails::Generators::Testing::Behaviour
+ include Rails::Generators::Testing::Assertions
+ include FileUtils
+
+ tests described_class
+ destination File.expand_path('tmp', __dir__)
+
+ before do
+ prepare_destination
+ end
+
+ after do
+ rm_rf(destination_root)
+ end
+
+ context 'with valid arguments' do
+ let(:expected_migration_file) { load_expected_file('queue_my_batched_migration.txt') }
+ let(:expected_migration_spec_file) { load_expected_file('queue_my_batched_migration_spec.txt') }
+ let(:expected_migration_job_file) { load_expected_file('my_batched_migration.txt') }
+ let(:expected_migration_job_spec_file) { load_expected_file('my_batched_migration_spec_matcher.txt') }
+ let(:expected_migration_dictionary) { load_expected_file('my_batched_migration_dictionary_matcher.txt') }
+
+ it 'generates expected files' do
+ run_generator %w[my_batched_migration --table_name=projects --column_name=id --feature_category=database]
+
+ assert_migration('db/post_migrate/queue_my_batched_migration.rb') do |migration_file|
+ expect(migration_file).to eq(expected_migration_file)
+ end
+
+ assert_migration('spec/migrations/queue_my_batched_migration_spec.rb') do |migration_spec_file|
+ expect(migration_spec_file).to eq(expected_migration_spec_file)
+ end
+
+ assert_file('lib/gitlab/background_migration/my_batched_migration.rb') do |migration_job_file|
+ expect(migration_job_file).to eq(expected_migration_job_file)
+ end
+
+ assert_file('spec/lib/gitlab/background_migration/my_batched_migration_spec.rb') do |migration_job_spec_file|
+ # Regex is used to match the dynamic schema: <version> in the specs
+ expect(migration_job_spec_file).to match(/#{expected_migration_job_spec_file}/)
+ end
+
+ assert_file('db/docs/batched_background_migrations/my_batched_migration.yml') do |migration_dictionary|
+ # Regex is used to match the dynamically generated 'milestone' in the dictionary
+ expect(migration_dictionary).to match(/#{expected_migration_dictionary}/)
+ end
+ end
+ end
+
+ context 'without required arguments' do
+ it 'throws table_name is required error' do
+ expect do
+ run_generator %w[my_batched_migration]
+ end.to raise_error(ArgumentError, 'table_name is required')
+ end
+
+ it 'throws column_name is required error' do
+ expect do
+ run_generator %w[my_batched_migration --table_name=projects]
+ end.to raise_error(ArgumentError, 'column_name is required')
+ end
+
+ it 'throws feature_category is required error' do
+ expect do
+ run_generator %w[my_batched_migration --table_name=projects --column_name=id]
+ end.to raise_error(ArgumentError, 'feature_category is required')
+ end
+ end
+
+ private
+
+ def load_expected_file(file_name)
+ File.read(File.expand_path("expected_files/#{file_name}", __dir__))
+ end
+end
diff --git a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration.txt
new file mode 100644
index 00000000000..b2378b414b1
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration.txt
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/database/batched_background_migrations.html
+# for more information on how to use batched background migrations
+
+# Update below commented lines with appropriate values.
+
+module Gitlab
+ module BackgroundMigration
+ class MyBatchedMigration < BatchedMigrationJob
+ # operation_name :my_operation
+ # scope_to ->(relation) { relation.where(column: "value") }
+ feature_category :database
+
+ def perform
+ each_sub_batch do |sub_batch|
+ # Your action on each sub_batch
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt
new file mode 100644
index 00000000000..6280d35177e
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_dictionary_matcher.txt
@@ -0,0 +1,6 @@
+---
+migration_job_name: MyBatchedMigration
+description: # Please capture what MyBatchedMigration does
+feature_category: database
+introduced_by_url: # URL of the MR \(or issue/commit\) that introduced the migration
+milestone: [0-9\.]+
diff --git a/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_spec_matcher.txt b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_spec_matcher.txt
new file mode 100644
index 00000000000..2728d65d54b
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/my_batched_migration_spec_matcher.txt
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MyBatchedMigration, schema: [0-9]+, feature_category: :database do # rubocop:disable Layout/LineLength
+ # Tests go here
+end
diff --git a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
new file mode 100644
index 00000000000..536e07d56aa
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/database/batched_background_migrations.html
+# for more information on when/how to queue batched background migrations
+
+# Update below commented lines with appropriate values.
+
+class QueueMyBatchedMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = "MyBatchedMigration"
+ # DELAY_INTERVAL = 2.minutes
+ # BATCH_SIZE = 1000
+ # SUB_BATCH_SIZE = 100
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :projects,
+ :id,
+ job_interval: DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(MIGRATION, :projects, :id, [])
+ end
+end
diff --git a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration_spec.txt b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration_spec.txt
new file mode 100644
index 00000000000..6f33de4ae83
--- /dev/null
+++ b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration_spec.txt
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueMyBatchedMigration, feature_category: :database do
+ # let!(:batched_migration) { described_class::MIGRATION }
+
+ # it 'schedules a 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: :projects,
+ # column_name: :id,
+ # interval: described_class::DELAY_INTERVAL,
+ # batch_size: described_class::BATCH_SIZE,
+ # sub_batch_size: described_class::SUB_BATCH_SIZE
+ # )
+ # }
+ # end
+ # end
+end
diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
index d9fa6b931ad..6826006949e 100644
--- a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
+RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout, feature_category: :product_analytics do
let(:ce_temp_dir) { Dir.mktmpdir }
let(:ee_temp_dir) { Dir.mktmpdir }
- let(:timestamp) { Time.current.to_i }
+ let(:timestamp) { Time.now.utc.strftime('%Y%m%d%H%M%S') }
let(:generator_options) { { 'category' => 'Groups::EmailCampaignsController', 'action' => 'click' } }
before do
@@ -30,7 +30,8 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
let(:file_name) { Dir.children(ce_temp_dir).first }
it 'creates CE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw!
+ sample_event = ::Gitlab::Config::Loader::Yaml
+ .new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw!
described_class.new([], generator_options).invoke_all
@@ -62,25 +63,13 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
end
end
- context 'event definition already exists' do
+ context 'when event definition with same file name already exists' do
before do
stub_const('Gitlab::VERSION', '12.11.0-pre')
described_class.new([], generator_options).invoke_all
end
- it 'overwrites event definition --force flag set to true' do
- sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw!
-
- stub_const('Gitlab::VERSION', '13.11.0-pre')
- described_class.new([], generator_options.merge('force' => true)).invoke_all
-
- event_definition_path = File.join(ce_temp_dir, file_name)
- event_data = ::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!
-
- expect(event_data).to eq(sample_event)
- end
-
- it 'raises error when --force flag set to false' do
+ it 'raises error' do
expect { described_class.new([], generator_options.merge('force' => false)).invoke_all }
.to raise_error(StandardError, /Event definition already exists at/)
end
@@ -90,7 +79,8 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
let(:file_name) { Dir.children(ee_temp_dir).first }
it 'creates EE event definition file using the template' do
- sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw!
+ sample_event = ::Gitlab::Config::Loader::Yaml
+ .new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw!
described_class.new([], generator_options.merge('ee' => true)).invoke_all
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
index de325454b34..122a94a39c2 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average do
subject(:average_duration_in_seconds) { average.seconds }
context 'when no results' do
- let(:query) { Issue.none }
+ let(:query) { Issue.joins(:metrics).none }
it { is_expected.to eq(nil) }
end
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average do
subject(:average_duration_in_days) { average.days }
context 'when no results' do
- let(:query) { Issue.none }
+ let(:query) { Issue.joins(:metrics).none }
it { is_expected.to eq(nil) }
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb
new file mode 100644
index 00000000000..3c171d684d6
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Analytics::CycleAnalytics::RequestParams, feature_category: :value_stream_management do
+ it_behaves_like 'unlicensed cycle analytics request params' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:root_group) { create(:group) }
+ let_it_be_with_refind(:project) { create(:project, group: root_group) }
+
+ let(:namespace) { project.project_namespace }
+
+ describe 'project-level data attributes' do
+ subject(:attributes) { described_class.new(params).to_data_attributes }
+
+ it 'includes the namespace attribute' do
+ expect(attributes).to match(hash_including({
+ namespace: {
+ name: project.name,
+ full_path: project.full_path
+ }
+ }))
+ end
+ 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 1e0034e386e..24248c557bd 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,8 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do
+RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent, feature_category: :product_analytics do
let(:instance) { described_class.new({}) }
it { expect(described_class).to respond_to(:name) }
diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
index c0c8e7aba63..48cae42dcd2 100644
--- a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::APIAuthentication::TokenResolver, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::APIAuthentication::TokenResolver, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
diff --git a/spec/lib/gitlab/app_logger_spec.rb b/spec/lib/gitlab/app_logger_spec.rb
index 85ca60d539f..e3415f4ad8c 100644
--- a/spec/lib/gitlab/app_logger_spec.rb
+++ b/spec/lib/gitlab/app_logger_spec.rb
@@ -5,22 +5,27 @@ require 'spec_helper'
RSpec.describe Gitlab::AppLogger do
subject { described_class }
- it 'builds two Logger instances' do
- expect(Gitlab::Logger).to receive(:new).and_call_original
- expect(Gitlab::JsonLogger).to receive(:new).and_call_original
+ context 'when UNSTRUCTURED_RAILS_LOG is enabled' do
+ before do
+ stub_env('UNSTRUCTURED_RAILS_LOG', 'true')
+ end
- subject.info('Hello World!')
- end
+ it 'builds two Logger instances' do
+ expect(Gitlab::Logger).to receive(:new).and_call_original
+ expect(Gitlab::JsonLogger).to receive(:new).and_call_original
- it 'logs info to AppLogger and AppJsonLogger' do
- expect_any_instance_of(Gitlab::AppTextLogger).to receive(:info).and_call_original
- expect_any_instance_of(Gitlab::AppJsonLogger).to receive(:info).and_call_original
+ subject.info('Hello World!')
+ end
- subject.info('Hello World!')
+ it 'logs info to AppLogger and AppJsonLogger' do
+ expect_any_instance_of(Gitlab::AppTextLogger).to receive(:info).and_call_original
+ expect_any_instance_of(Gitlab::AppJsonLogger).to receive(:info).and_call_original
+
+ subject.info('Hello World!')
+ end
end
it 'logs info to only the AppJsonLogger when unstructured logs are disabled' do
- stub_env('UNSTRUCTURED_RAILS_LOG', 'false')
expect_any_instance_of(Gitlab::AppTextLogger).not_to receive(:info).and_call_original
expect_any_instance_of(Gitlab::AppJsonLogger).to receive(:info).and_call_original
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index cb9d1e9eae8..21cd87e89dc 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -369,7 +369,7 @@ module Gitlab
<div>
<div>
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre lang="javascript" class="code highlight js-syntax-highlight language-javascript" data-canonical-lang="js" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ <pre lang="javascript" class="code highlight js-syntax-highlight language-javascript" data-canonical-lang="js" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
<copy-code></copy-code>
</div>
</div>
diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb
index 4b16333d913..2b3c8506440 100644
--- a/spec/lib/gitlab/audit/auditor_spec.rb
+++ b/spec/lib/gitlab/audit/auditor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Audit::Auditor do
+RSpec.describe Gitlab::Audit::Auditor, feature_category: :audit_events do
let(:name) { 'audit_operation' }
let(:author) { create(:user, :with_sign_ins) }
let(:group) { create(:group) }
@@ -22,9 +22,9 @@ RSpec.describe Gitlab::Audit::Auditor do
subject(:auditor) { described_class }
describe '.audit' do
- context 'when authentication event' do
- let(:audit!) { auditor.audit(context) }
+ let(:audit!) { auditor.audit(context) }
+ context 'when authentication event' do
it 'creates an authentication event' do
expect(AuthenticationEvent).to receive(:new).with(
{
@@ -210,19 +210,38 @@ RSpec.describe Gitlab::Audit::Auditor do
end
context 'when authentication event is false' do
+ let(:target) { group }
let(:context) do
{ name: name, author: author, scope: group,
- target: group, authentication_event: false, message: "sample message" }
+ target: target, authentication_event: false, message: "sample message" }
end
it 'does not create an authentication event' do
expect { auditor.audit(context) }.not_to change(AuthenticationEvent, :count)
end
+
+ context 'with permitted target' do
+ { feature_flag: :operations_feature_flag }.each do |target_type, factory_name|
+ context "with #{target_type}" do
+ let(:target) { build_stubbed factory_name }
+
+ it 'logs audit events to database', :aggregate_failures, :freeze_time do
+ audit!
+ audit_event = AuditEvent.last
+
+ expect(audit_event.author_id).to eq(author.id)
+ expect(audit_event.entity_id).to eq(group.id)
+ expect(audit_event.entity_type).to eq(group.class.name)
+ expect(audit_event.created_at).to eq(Time.zone.now)
+ expect(audit_event.details[:target_id]).to eq(target.id)
+ expect(audit_event.details[:target_type]).to eq(target.class.name)
+ end
+ end
+ end
+ end
end
context 'when authentication event is invalid' do
- let(:audit!) { auditor.audit(context) }
-
before do
allow(AuthenticationEvent).to receive(:new).and_raise(ActiveRecord::RecordInvalid)
allow(Gitlab::ErrorTracking).to receive(:track_exception)
@@ -243,8 +262,6 @@ RSpec.describe Gitlab::Audit::Auditor do
end
context 'when audit events are invalid' do
- let(:audit!) { auditor.audit(context) }
-
before do
expect_next_instance_of(AuditEvent) do |instance|
allow(instance).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 6aedd0a0a23..ecc5c688228 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
include described_class
include HttpBasicAuthHelpers
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 04fbbff3559..8a86b306604 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :system_access do
include LdapHelpers
let(:oauth_user) { described_class.new(auth_hash) }
diff --git a/spec/lib/gitlab/auth/otp/strategies/duo_auth/manual_otp_spec.rb b/spec/lib/gitlab/auth/otp/strategies/duo_auth/manual_otp_spec.rb
new file mode 100644
index 00000000000..d04e0ad9fb4
--- /dev/null
+++ b/spec/lib/gitlab/auth/otp/strategies/duo_auth/manual_otp_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::Otp::Strategies::DuoAuth::ManualOtp, feature_category: :system_access do
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:otp_code) { 42 }
+
+ let_it_be(:hostname) { 'duo_auth.example.com' }
+ let_it_be(:integration_key) { 'int3gr4t1on' }
+ let_it_be(:secret_key) { 's3cr3t' }
+
+ let_it_be(:duo_response_builder) { Struct.new(:body) }
+
+ let_it_be(:response_status) { 200 }
+
+ let_it_be(:duo_auth_url) { "https://#{hostname}/auth/v2/auth/" }
+ let_it_be(:params) do
+ { username: user.username,
+ factor: "passcode",
+ passcode: otp_code }
+ end
+
+ let_it_be(:manual_otp) { described_class.new(user) }
+
+ subject(:response) { manual_otp.validate(otp_code) }
+
+ before do
+ stub_duo_auth_config(
+ enabled: true,
+ hostname: hostname,
+ secret_key: secret_key,
+ integration_key: integration_key
+ )
+ end
+
+ context 'when successful validation' do
+ before do
+ allow(duo_client).to receive(:request)
+ .with("POST", "/auth/v2/auth", params)
+ .and_return(duo_response_builder.new('{ "response": { "result": "allow" }}'))
+
+ allow(manual_otp).to receive(:duo_client).and_return(duo_client)
+ end
+
+ it 'returns success' do
+ response
+
+ expect(response[:status]).to eq(:success)
+ end
+ end
+
+ context 'when unsuccessful validation' do
+ before do
+ allow(duo_client).to receive(:request)
+ .with("POST", "/auth/v2/auth", params)
+ .and_return(duo_response_builder.new('{ "response": { "result": "deny" }}'))
+
+ allow(manual_otp).to receive(:duo_client).and_return(duo_client)
+ end
+
+ it 'returns error' do
+ response
+
+ expect(response[:status]).to eq(:error)
+ end
+ end
+
+ context 'when unexpected error' do
+ before do
+ allow(duo_client).to receive(:request)
+ .with("POST", "/auth/v2/auth", params)
+ .and_return(duo_response_builder.new('aaa'))
+
+ allow(manual_otp).to receive(:duo_client).and_return(duo_client)
+ end
+
+ it 'returns error' do
+ response
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:message]).to match(/unexpected character/)
+ end
+ end
+
+ def stub_duo_auth_config(duo_auth_settings)
+ allow(::Gitlab.config.duo_auth).to(receive_messages(duo_auth_settings))
+ end
+
+ def duo_client
+ manual_otp.send(:duo_client)
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index a5f46aa1f35..11e9ecdb878 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_category: :system_access do
let_it_be(:project) { create(:project) }
let(:auth_failure) { { actor: nil, project: nil, type: nil, authentication_abilities: nil } }
diff --git a/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb b/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb
index 7075d4694ae..d2da6867773 100644
--- a/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillAdminModeScopeForPersonalAccessTokens,
- :migration, schema: 20221228103133, feature_category: :authentication_and_authorization do
+ :migration, schema: 20221228103133, feature_category: :system_access do
let(:users) { table(:users) }
let(:personal_access_tokens) { table(:personal_access_tokens) }
diff --git a/spec/lib/gitlab/background_migration/backfill_prepared_at_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_prepared_at_merge_requests_spec.rb
new file mode 100644
index 00000000000..b33a1a31c40
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_prepared_at_merge_requests_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillPreparedAtMergeRequests, :migration,
+ feature_category: :code_review_workflow, schema: 20230202135758 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:mr_table) { table(:merge_requests) }
+
+ let(:namespace) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') }
+ let(:proj_namespace) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace.id) }
+ let(:project) do
+ projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace.id, project_namespace_id: proj_namespace.id)
+ end
+
+ let(:test_worker) do
+ described_class.new(
+ start_id: 1,
+ end_id: 100,
+ batch_table: :merge_requests,
+ batch_column: :id,
+ sub_batch_size: 10,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ it 'updates merge requests with prepared_at nil' do
+ time = Time.current
+
+ mr_1 = mr_table.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature',
+ prepared_at: nil, merge_status: 'checking')
+ mr_2 = mr_table.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature',
+ prepared_at: nil, merge_status: 'preparing')
+ mr_3 = mr_table.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature',
+ prepared_at: time)
+ mr_4 = mr_table.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature',
+ prepared_at: time, merge_status: 'checking')
+ mr_5 = mr_table.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature',
+ prepared_at: time, merge_status: 'preparing')
+
+ expect(mr_1.prepared_at).to be_nil
+ expect(mr_2.prepared_at).to be_nil
+ expect(mr_3.prepared_at.to_i).to eq(time.to_i)
+ expect(mr_4.prepared_at.to_i).to eq(time.to_i)
+ expect(mr_5.prepared_at.to_i).to eq(time.to_i)
+
+ test_worker.perform
+
+ expect(mr_1.reload.prepared_at.to_i).to eq(mr_1.created_at.to_i)
+ expect(mr_2.reload.prepared_at).to be_nil
+ expect(mr_3.reload.prepared_at.to_i).to eq(time.to_i)
+ expect(mr_4.reload.prepared_at.to_i).to eq(time.to_i)
+ expect(mr_5.reload.prepared_at.to_i).to eq(time.to_i)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_wiki_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_wiki_repositories_spec.rb
new file mode 100644
index 00000000000..e81bd0604e6
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_wiki_repositories_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe(
+ Gitlab::BackgroundMigration::BackfillProjectWikiRepositories,
+ schema: 20230306195007,
+ feature_category: :geo_replication) do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:project_wiki_repositories) { table(:project_wiki_repositories) }
+
+ subject(:migration) do
+ described_class.new(
+ start_id: projects.minimum(:id),
+ end_id: projects.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ )
+ end
+
+ describe '#perform' do
+ it 'creates project_wiki_repositories entries for all projects in range' do
+ namespace1 = create_namespace('test1')
+ namespace2 = create_namespace('test2')
+ project1 = create_project(namespace1, 'test1')
+ project2 = create_project(namespace2, 'test2')
+ project_wiki_repositories.create!(project_id: project2.id)
+
+ expect { migration.perform }
+ .to change { project_wiki_repositories.pluck(:project_id) }
+ .from([project2.id])
+ .to match_array([project1.id, project2.id])
+ end
+
+ it 'does nothing if project_id already exist in project_wiki_repositories' do
+ namespace = create_namespace('test1')
+ project = create_project(namespace, 'test1')
+ project_wiki_repositories.create!(project_id: project.id)
+
+ expect { migration.perform }
+ .not_to change { project_wiki_repositories.pluck(:project_id) }
+ end
+
+ def create_namespace(name)
+ namespaces.create!(
+ name: name,
+ path: name,
+ type: 'Project'
+ )
+ end
+
+ def create_project(namespace, name)
+ projects.create!(
+ namespace_id: namespace.id,
+ project_namespace_id: namespace.id,
+ name: name,
+ path: name
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
index faaaccfdfaf..781bf93dd85 100644
--- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
@@ -301,6 +301,28 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
perform_job
end
+ context 'when using a sub batch exception for timeouts' do
+ let(:job_class) do
+ Class.new(described_class) do
+ operation_name :update
+
+ def perform(*_)
+ each_sub_batch { raise ActiveRecord::StatementTimeout } # rubocop:disable Lint/UnreachableLoop
+ end
+ end
+ end
+
+ let(:job_instance) do
+ job_class.new(start_id: 1, end_id: 10, batch_table: '_test_table', batch_column: 'id',
+ sub_batch_size: 2, pause_ms: 1000, connection: connection,
+ sub_batch_exception: StandardError)
+ end
+
+ it 'raises the expected error type' do
+ expect { job_instance.perform }.to raise_error(StandardError)
+ end
+ end
+
context 'when batching_arguments are given' do
it 'forwards them for batching' do
expect(job_instance).to receive(:base_relation).and_return(test_table)
diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_packages_dependencies_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_packages_dependencies_spec.rb
new file mode 100644
index 00000000000..0d82717c7de
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_orphaned_packages_dependencies_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedPackagesDependencies, schema: 20230303105806,
+ feature_category: :package_registry do
+ let!(:migration_attrs) do
+ {
+ start_id: 1,
+ end_id: 1000,
+ batch_table: :packages_dependencies,
+ batch_column: :id,
+ sub_batch_size: 500,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ }
+ end
+
+ let!(:migration) { described_class.new(**migration_attrs) }
+
+ let(:packages_dependencies) { table(:packages_dependencies) }
+
+ let!(:namespace) { table(:namespaces).create!(name: 'project', path: 'project', type: 'Project') }
+ let!(:project) do
+ table(:projects).create!(name: 'project', path: 'project', project_namespace_id: namespace.id,
+ namespace_id: namespace.id)
+ end
+
+ let!(:package) do
+ table(:packages_packages).create!(name: 'test', version: '1.2.3', package_type: 2, project_id: project.id)
+ end
+
+ let!(:orphan_dependency_1) { packages_dependencies.create!(name: 'dependency 1', version_pattern: '~0.0.1') }
+ let!(:orphan_dependency_2) { packages_dependencies.create!(name: 'dependency 2', version_pattern: '~0.0.2') }
+ let!(:orphan_dependency_3) { packages_dependencies.create!(name: 'dependency 3', version_pattern: '~0.0.3') }
+ let!(:linked_dependency) do
+ packages_dependencies.create!(name: 'dependency 4', version_pattern: '~0.0.4').tap do |dependency|
+ table(:packages_dependency_links).create!(package_id: package.id, dependency_id: dependency.id,
+ dependency_type: 'dependencies')
+ end
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ it 'executes 3 queries' do
+ queries = ActiveRecord::QueryRecorder.new do
+ perform_migration
+ end
+
+ expect(queries.count).to eq(3)
+ end
+
+ it 'deletes only orphaned dependencies' do
+ expect { perform_migration }.to change { packages_dependencies.count }.by(-3)
+ expect(packages_dependencies.all).to eq([linked_dependency])
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/fix_vulnerability_reads_has_issues_spec.rb b/spec/lib/gitlab/background_migration/fix_vulnerability_reads_has_issues_spec.rb
new file mode 100644
index 00000000000..9f431c43f39
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_vulnerability_reads_has_issues_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityReadsHasIssues, schema: 20230302185739, feature_category: :vulnerability_management do # rubocop:disable Layout/LineLength
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:work_item_types) { table(:work_item_types) }
+ let(:issues) { table(:issues) }
+ let(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
+
+ let(:namespace) { namespaces.create!(name: 'user', path: 'user') }
+ let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
+ let(:user) { users.create!(username: 'john_doe', email: 'johndoe@gitlab.com', projects_limit: 10) }
+ let(:scanner) { scanners.create!(project_id: project.id, external_id: 'external_id', name: 'Test Scanner') }
+ let(:work_item_type) { work_item_types.create!(name: 'test') }
+
+ let(:vulnerability_records) do
+ Array.new(4).map do |_, n|
+ vulnerabilities.create!(
+ project_id: project.id,
+ author_id: user.id,
+ title: "vulnerability #{n}",
+ severity: 1,
+ confidence: 1,
+ report_type: 1
+ )
+ end
+ end
+
+ let(:vulnerabilities_with_issues) { [vulnerability_records.first, vulnerability_records.third] }
+ let(:vulnerabilities_without_issues) { vulnerability_records - vulnerabilities_with_issues }
+
+ let(:vulnerability_read_records) do
+ vulnerability_records.map do |vulnerability|
+ vulnerability_reads.create!(
+ project_id: project.id,
+ vulnerability_id: vulnerability.id,
+ scanner_id: scanner.id,
+ has_issues: false,
+ severity: 1,
+ report_type: 1,
+ state: 1,
+ uuid: SecureRandom.uuid
+ )
+ end
+ end
+
+ let!(:issue_links) do
+ vulnerabilities_with_issues.map do |vulnerability|
+ issue = issues.create!(
+ title: vulnerability.title,
+ author_id: user.id,
+ project_id: project.id,
+ confidential: true,
+ work_item_type_id: work_item_type.id,
+ namespace_id: namespace.id
+ )
+
+ vulnerability_issue_links.create!(
+ vulnerability_id: vulnerability.id,
+ issue_id: issue.id
+ )
+ end
+ end
+
+ def vulnerability_read_for(vulnerability)
+ vulnerability_read_records.find { |read| read.vulnerability_id == vulnerability.id }
+ end
+
+ subject(:perform_migration) do
+ described_class.new(
+ start_id: issue_links.first.vulnerability_id,
+ end_id: issue_links.last.vulnerability_id,
+ batch_table: :vulnerability_issue_links,
+ batch_column: :vulnerability_id,
+ sub_batch_size: issue_links.size,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
+ end
+
+ it 'only changes records with issue links' do
+ expect(vulnerability_read_records).to all(have_attributes(has_issues: false))
+
+ perform_migration
+
+ vulnerabilities_with_issues.each do |vulnerability|
+ expect(vulnerability_read_for(vulnerability).reload.has_issues).to eq(true)
+ end
+
+ vulnerabilities_without_issues.each do |vulnerability|
+ expect(vulnerability_read_for(vulnerability).reload.has_issues).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb b/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb
new file mode 100644
index 00000000000..1adff322b41
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+# this needs the schema to be before we introduce the not null constraint on routes#namespace_id
+# rubocop:disable RSpec/MultipleMemoizedHelpers
+RSpec.describe Gitlab::BackgroundMigration::IssuesInternalIdScopeUpdater, feature_category: :team_planning do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:internal_ids) { table(:internal_ids) }
+
+ let(:gr1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') }
+ let(:gr2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: gr1.id, path: 'space2') }
+
+ let(:pr_nmsp1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: gr1.id) }
+ let(:pr_nmsp2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: gr1.id) }
+ let(:pr_nmsp3) { namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: gr2.id) }
+ let(:pr_nmsp4) { namespaces.create!(name: 'proj4', path: 'proj4', type: 'Project', parent_id: gr2.id) }
+ let(:pr_nmsp5) { namespaces.create!(name: 'proj5', path: 'proj5', type: 'Project', parent_id: gr2.id) }
+ let(:pr_nmsp6) { namespaces.create!(name: 'proj6', path: 'proj6', type: 'Project', parent_id: gr2.id) }
+
+ # rubocop:disable Layout/LineLength
+ let(:p1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: gr1.id, project_namespace_id: pr_nmsp1.id) }
+ let(:p2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: gr1.id, project_namespace_id: pr_nmsp2.id) }
+ let(:p3) { projects.create!(name: 'proj3', path: 'proj3', namespace_id: gr2.id, project_namespace_id: pr_nmsp3.id) }
+ let(:p4) { projects.create!(name: 'proj4', path: 'proj4', namespace_id: gr2.id, project_namespace_id: pr_nmsp4.id) }
+ let(:p5) { projects.create!(name: 'proj5', path: 'proj5', namespace_id: gr2.id, project_namespace_id: pr_nmsp5.id) }
+ let(:p6) { projects.create!(name: 'proj6', path: 'proj6', namespace_id: gr2.id, project_namespace_id: pr_nmsp6.id) }
+ # rubocop:enable Layout/LineLength
+
+ # a project that already is covered by a record for its namespace. This should result in no new record added and
+ # project related record deleted
+ let!(:issues_internal_ids_p1) { internal_ids.create!(project_id: p1.id, usage: 0, last_value: 100) }
+ let!(:issues_internal_ids_pr_nmsp1) { internal_ids.create!(namespace_id: pr_nmsp1.id, usage: 0, last_value: 111) }
+
+ # project records that do not have a corresponding namespace record. This should result 2 new records
+ # scoped to corresponding project namespaces being added and the project related records being deleted.
+ let!(:issues_internal_ids_p2) { internal_ids.create!(project_id: p2.id, usage: 0, last_value: 200) }
+ let!(:issues_internal_ids_p3) { internal_ids.create!(project_id: p3.id, usage: 0, last_value: 300) }
+
+ # a project record on a different usage, should not be affected by the migration and
+ # no new record should be created for this case
+ let!(:issues_internal_ids_p4) { internal_ids.create!(project_id: p4.id, usage: 4, last_value: 400) }
+
+ # a project namespace scoped record without a corresponding project record, should not affect anything.
+ let!(:issues_internal_ids_pr_nmsp5) { internal_ids.create!(namespace_id: pr_nmsp5.id, usage: 0, last_value: 500) }
+
+ # a record scoped to a group, should not affect anything.
+ let!(:issues_internal_ids_gr1) { internal_ids.create!(namespace_id: gr1.id, usage: 0, last_value: 600) }
+
+ # a project that is covered by a record for its namespace, but has a higher last_value, due to updates during rolling
+ # deploy for instance, see https://gitlab.com/gitlab-com/gl-infra/production/-/issues/8548
+ let!(:issues_internal_ids_p6) { internal_ids.create!(project_id: p6.id, usage: 0, last_value: 111) }
+ let!(:issues_internal_ids_pr_nmsp6) { internal_ids.create!(namespace_id: pr_nmsp6.id, usage: 0, last_value: 100) }
+
+ subject(:perform_migration) do
+ described_class.new(
+ start_id: internal_ids.minimum(:id),
+ end_id: internal_ids.maximum(:id),
+ batch_table: :internal_ids,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection
+ ).perform
+ end
+
+ it 'backfills internal_ids records and removes related project records', :aggregate_failures do
+ perform_migration
+
+ expected_recs = [pr_nmsp1.id, pr_nmsp2.id, pr_nmsp3.id, pr_nmsp5.id, gr1.id, pr_nmsp6.id]
+
+ # all namespace scoped records for issues(0) usage
+ expect(internal_ids.where.not(namespace_id: nil).where(usage: 0).count).to eq(6)
+ # all namespace_ids for issues(0) usage
+ expect(internal_ids.where.not(namespace_id: nil).where(usage: 0).pluck(:namespace_id)).to match_array(expected_recs)
+ # this is the record with usage: 4
+ expect(internal_ids.where.not(project_id: nil).count).to eq(1)
+ # no project scoped records for issues usage left
+ expect(internal_ids.where.not(project_id: nil).where(usage: 0).count).to eq(0)
+
+ # the case when the project_id scoped record had the higher last_value,
+ # see `issues_internal_ids_p6` and issues_internal_ids_pr_nmsp6 definitions above
+ expect(internal_ids.where(namespace_id: pr_nmsp6.id).first.last_value).to eq(111)
+
+ # the case when the namespace_id scoped record had the higher last_value,
+ # see `issues_internal_ids_p1` and issues_internal_ids_pr_nmsp1 definitions above.
+ expect(internal_ids.where(namespace_id: pr_nmsp1.id).first.last_value).to eq(111)
+ end
+end
+# rubocop:enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb b/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb
new file mode 100644
index 00000000000..b70044ab2a4
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MigrateEvidencesForVulnerabilityFindings,
+ feature_category: :vulnerability_management do
+ let(:vulnerability_occurrences) { table(:vulnerability_occurrences) }
+ let(:vulnerability_finding_evidences) { table(:vulnerability_finding_evidences) }
+ let(:evidence_hash) { { url: 'http://test.com' } }
+ let(:namespace1) { table(:namespaces).create!(name: 'namespace 1', path: 'namespace1') }
+ let(:project1) { table(:projects).create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id) }
+ let(:user) { table(:users).create!(email: 'test1@example.com', projects_limit: 5) }
+
+ let(:scanner1) do
+ table(:vulnerability_scanners).create!(project_id: project1.id, external_id: 'test 1', name: 'test scanner 1')
+ end
+
+ let(:stating_id) { vulnerability_occurrences.pluck(:id).min }
+ let(:end_id) { vulnerability_occurrences.pluck(:id).max }
+
+ let(:migration) do
+ described_class.new(
+ start_id: stating_id,
+ end_id: end_id,
+ batch_table: :vulnerability_occurrences,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 2,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ context 'without the presence of evidence key' do
+ before do
+ create_finding!(project1.id, scanner1.id, { other_keys: 'test' })
+ end
+
+ it 'does not create any evidence' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_evidences.count }
+ end
+ end
+
+ context 'with evidence equals to nil' do
+ before do
+ create_finding!(project1.id, scanner1.id, { evidence: nil })
+ end
+
+ it 'does not create any evidence' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_evidences.count }
+ end
+ end
+
+ context 'with existing evidence within raw_metadata' do
+ let!(:finding1) { create_finding!(project1.id, scanner1.id, { evidence: evidence_hash }) }
+ let!(:finding2) { create_finding!(project1.id, scanner1.id, { evidence: evidence_hash }) }
+
+ it 'creates new evidence for each finding' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_finding_evidences.count }.by(2)
+ end
+
+ context 'when create throws exception StandardError' do
+ before do
+ allow(migration).to receive(:create_evidences).and_raise(StandardError)
+ end
+
+ it 'logs StandardError' do
+ expect(Gitlab::AppLogger).to receive(:error).with({
+ class: described_class.name, message: StandardError.to_s
+ })
+ expect { perform_migration }.not_to change { vulnerability_finding_evidences.count }
+ end
+ end
+
+ context 'when parse throws exception JSON::ParserError' do
+ before do
+ allow(Gitlab::Json).to receive(:parse).and_raise(JSON::ParserError)
+ end
+
+ it 'does not log this error nor create new records' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_evidences.count }
+ end
+ end
+ end
+
+ context 'with existing evidence records' do
+ let!(:finding) { create_finding!(project1.id, scanner1.id, { evidence: evidence_hash }) }
+
+ before do
+ vulnerability_finding_evidences.create!(vulnerability_occurrence_id: finding.id, data: evidence_hash)
+ end
+
+ it 'does not create new evidence' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_evidences.count }
+ end
+
+ context 'with non-existing evidence' do
+ let!(:finding3) { create_finding!(project1.id, scanner1.id, { evidence: { url: 'http://secondary.com' } }) }
+
+ it 'creates a new evidence only to the non-existing evidence' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_finding_evidences.count }.by(1)
+ end
+ end
+ end
+
+ private
+
+ def create_finding!(project_id, scanner_id, raw_metadata)
+ vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
+ severity: 4, confidence: 4, report_type: 0)
+
+ identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(vulnerability.id.to_s),
+ name: 'Identifier for UUIDv5 2 2')
+
+ table(:vulnerability_occurrences).create!(
+ vulnerability_id: vulnerability.id, project_id: project_id, scanner_id: scanner_id,
+ primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
+ uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
+ location_fingerprint: 'test', metadata_version: 'test',
+ raw_metadata: raw_metadata.to_json)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb b/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb
new file mode 100644
index 00000000000..fd2e3ffb670
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MigrateLinksForVulnerabilityFindings,
+ feature_category: :vulnerability_management do
+ let(:vulnerability_occurrences) { table(:vulnerability_occurrences) }
+ let(:vulnerability_finding_links) { table(:vulnerability_finding_links) }
+ let(:link_hash) { { url: 'http://test.com' } }
+ let(:namespace1) { table(:namespaces).create!(name: 'namespace 1', path: 'namespace1') }
+ let(:project1) { table(:projects).create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id) }
+ let(:user) { table(:users).create!(email: 'test1@example.com', projects_limit: 5) }
+
+ let(:scanner1) do
+ table(:vulnerability_scanners).create!(project_id: project1.id, external_id: 'test 1', name: 'test scanner 1')
+ end
+
+ let(:stating_id) { vulnerability_occurrences.pluck(:id).min }
+ let(:end_id) { vulnerability_occurrences.pluck(:id).max }
+
+ let(:migration) do
+ described_class.new(
+ start_id: stating_id,
+ end_id: end_id,
+ batch_table: :vulnerability_occurrences,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 2,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ context 'without the presence of links key' do
+ before do
+ create_finding!(project1.id, scanner1.id, { other_keys: 'test' })
+ end
+
+ it 'does not create any link' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_links.count }
+ end
+ end
+
+ context 'with links equals to an array of nil element' do
+ before do
+ create_finding!(project1.id, scanner1.id, { links: [nil] })
+ end
+
+ it 'does not create any link' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_links.count }
+ end
+ end
+
+ context 'with links equals to an array of duplicated elements' do
+ let!(:finding) do
+ create_finding!(project1.id, scanner1.id, { links: [link_hash, link_hash] })
+ end
+
+ it 'creates one new link' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_finding_links.count }.by(1)
+ end
+ end
+
+ context 'with existing links within raw_metadata' do
+ let!(:finding1) { create_finding!(project1.id, scanner1.id, { links: [link_hash] }) }
+ let!(:finding2) { create_finding!(project1.id, scanner1.id, { links: [link_hash] }) }
+
+ it 'creates new link for each finding' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_finding_links.count }.by(2)
+ end
+
+ context 'when create throws exception ActiveRecord::RecordNotUnique' do
+ before do
+ allow(migration).to receive(:create_links).and_raise(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'does not log this error nor create new records' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_links.count }
+ end
+ end
+
+ context 'when create throws exception StandardError' do
+ before do
+ allow(migration).to receive(:create_links).and_raise(StandardError)
+ end
+
+ it 'logs StandardError' do
+ expect(Gitlab::AppLogger).to receive(:error).with({
+ class: described_class.name, message: StandardError.to_s, model_id: finding1.id
+ })
+ expect(Gitlab::AppLogger).to receive(:error).with({
+ class: described_class.name, message: StandardError.to_s, model_id: finding2.id
+ })
+ expect { perform_migration }.not_to change { vulnerability_finding_links.count }
+ end
+ end
+ end
+
+ context 'with existing link records' do
+ let!(:finding) { create_finding!(project1.id, scanner1.id, { links: [link_hash] }) }
+
+ before do
+ vulnerability_finding_links.create!(vulnerability_occurrence_id: finding.id, url: link_hash[:url])
+ end
+
+ it 'does not create new link' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_finding_links.count }
+ end
+ end
+
+ private
+
+ def create_finding!(project_id, scanner_id, raw_metadata)
+ vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
+ severity: 4, confidence: 4, report_type: 0)
+
+ identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(vulnerability.id.to_s),
+ name: 'Identifier for UUIDv5 2 2')
+
+ table(:vulnerability_occurrences).create!(
+ vulnerability_id: vulnerability.id, project_id: project_id, scanner_id: scanner_id,
+ primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
+ uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
+ location_fingerprint: 'test', metadata_version: 'test',
+ raw_metadata: raw_metadata.to_json)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb b/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb
new file mode 100644
index 00000000000..b75c0e61b19
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MigrateRemediationsForVulnerabilityFindings,
+ feature_category: :vulnerability_management do
+ let(:vulnerability_occurrences) { table(:vulnerability_occurrences) }
+ let(:vulnerability_findings_remediations) { table(:vulnerability_findings_remediations) }
+ let(:vulnerability_remediations) { table(:vulnerability_remediations) }
+ let(:remediation_hash) { { summary: 'summary', diff: "ZGlmZiAtLWdp" } }
+ let(:namespace1) { table(:namespaces).create!(name: 'namespace 1', path: 'namespace1') }
+ let(:project1) { table(:projects).create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id) }
+ let(:user) { table(:users).create!(email: 'test1@example.com', projects_limit: 5) }
+
+ let(:scanner1) do
+ table(:vulnerability_scanners).create!(project_id: project1.id, external_id: 'test 1', name: 'test scanner 1')
+ end
+
+ let(:stating_id) { vulnerability_occurrences.pluck(:id).min }
+ let(:end_id) { vulnerability_occurrences.pluck(:id).max }
+
+ let(:migration) do
+ described_class.new(
+ start_id: stating_id,
+ end_id: end_id,
+ batch_table: :vulnerability_occurrences,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 2,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ context 'without the presence of remediation key' do
+ before do
+ create_finding!(project1.id, scanner1.id, { other_keys: 'test' })
+ end
+
+ it 'does not create any remediation' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_remediations.count }
+ end
+ end
+
+ context 'with remediation equals to an array of nil element' do
+ before do
+ create_finding!(project1.id, scanner1.id, { remediations: [nil] })
+ end
+
+ it 'does not create any remediation' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_remediations.count }
+ end
+ end
+
+ context 'with remediation with empty string as the diff key' do
+ let!(:finding) do
+ create_finding!(project1.id, scanner1.id, { remediations: [{ summary: 'summary', diff: '' }] })
+ end
+
+ it 'does not create any remediation' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_remediations.count }
+ end
+ end
+
+ context 'with remediation equals to an array of duplicated elements' do
+ let!(:finding) do
+ create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash, remediation_hash] })
+ end
+
+ it 'creates new remediation' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_remediations.count }.by(1)
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding.id).length).to eq(1)
+ end
+ end
+
+ context 'with existing remediations within raw_metadata' do
+ let!(:finding1) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }
+ let!(:finding2) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }
+
+ it 'creates new remediation' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_remediations.count }.by(1)
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding1.id).length).to eq(1)
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding2.id).length).to eq(1)
+ end
+
+ context 'when create throws exception other than ActiveRecord::RecordNotUnique' do
+ before do
+ allow(migration).to receive(:create_finding_remediations).and_raise(StandardError)
+ end
+
+ it 'rolls back all related transactions' do
+ expect(Gitlab::AppLogger).to receive(:error).with({
+ class: described_class.name, message: StandardError.to_s, model_id: finding1.id
+ })
+ expect(Gitlab::AppLogger).to receive(:error).with({
+ class: described_class.name, message: StandardError.to_s, model_id: finding2.id
+ })
+ expect { perform_migration }.not_to change { vulnerability_remediations.count }
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding1.id).length).to eq(0)
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding2.id).length).to eq(0)
+ end
+ end
+ end
+
+ context 'with existing remediation records' do
+ let!(:finding) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }
+
+ before do
+ vulnerability_remediations.create!(project_id: project1.id, summary: remediation_hash[:summary],
+ checksum: checksum(remediation_hash[:diff]), file: Tempfile.new.path)
+ end
+
+ it 'does not create new remediation' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.not_to change { vulnerability_remediations.count }
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding.id).length).to eq(1)
+ end
+ end
+
+ context 'with same raw_metadata for different projects' do
+ let(:namespace2) { table(:namespaces).create!(name: 'namespace 2', path: 'namespace2') }
+ let(:project2) { table(:projects).create!(namespace_id: namespace2.id, project_namespace_id: namespace2.id) }
+ let(:scanner2) do
+ table(:vulnerability_scanners).create!(project_id: project2.id, external_id: 'test 2', name: 'test scanner 2')
+ end
+
+ let!(:finding1) { create_finding!(project1.id, scanner1.id, { remediations: [remediation_hash] }) }
+ let!(:finding2) { create_finding!(project2.id, scanner2.id, { remediations: [remediation_hash] }) }
+
+ it 'creates new remediation for each project' do
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect { perform_migration }.to change { vulnerability_remediations.count }.by(2)
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding1.id).length).to eq(1)
+ expect(vulnerability_findings_remediations.where(vulnerability_occurrence_id: finding2.id).length).to eq(1)
+ end
+ end
+
+ private
+
+ def create_finding!(project_id, scanner_id, raw_metadata)
+ vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
+ severity: 4, confidence: 4, report_type: 0)
+
+ identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(vulnerability.id.to_s),
+ name: 'Identifier for UUIDv5 2 2')
+
+ table(:vulnerability_occurrences).create!(
+ vulnerability_id: vulnerability.id, project_id: project_id, scanner_id: scanner_id,
+ primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
+ uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
+ location_fingerprint: 'test', metadata_version: 'test',
+ raw_metadata: raw_metadata.to_json)
+ end
+
+ def checksum(value)
+ sha = Digest::SHA256.hexdigest(value)
+ Gitlab::Database::ShaAttribute.new.serialize(sha)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
index a8574411957..f671a673a08 100644
--- a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects do
+RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects,
+ schema: 20230130073109 do
let(:users) { table(:users) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/lib/gitlab/background_task_spec.rb b/spec/lib/gitlab/background_task_spec.rb
index 102556b6b2f..da92fc9e765 100644
--- a/spec/lib/gitlab/background_task_spec.rb
+++ b/spec/lib/gitlab/background_task_spec.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
# We need to capture task state from a closure, which requires instance variables.
# rubocop: disable RSpec/InstanceVariable
-RSpec.describe Gitlab::BackgroundTask do
+RSpec.describe Gitlab::BackgroundTask, feature_category: :build do
let(:options) { {} }
let(:task) do
proc do
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 1526a1a9f2d..48ceda9e8d8 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -358,7 +358,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer, feature_category: :integration
describe 'issue import' do
it 'allocates internal ids' do
- expect(Issue).to receive(:track_project_iid!).with(project, 6)
+ expect(Issue).to receive(:track_namespace_iid!).with(project.project_namespace, 6)
importer.execute
end
diff --git a/spec/lib/gitlab/cache/client_spec.rb b/spec/lib/gitlab/cache/client_spec.rb
new file mode 100644
index 00000000000..ec22fcdee7e
--- /dev/null
+++ b/spec/lib/gitlab/cache/client_spec.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Cache::Client, feature_category: :source_code_management do
+ subject(:client) { described_class.new(metadata, backend: backend) }
+
+ let(:backend) { Rails.cache }
+ let(:metadata) do
+ Gitlab::Cache::Metadata.new(
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ )
+ end
+
+ let(:caller_id) { 'caller-id' }
+ let(:cache_identifier) { 'MyClass#cache' }
+ let(:feature_category) { :source_code_management }
+ let(:backing_resource) { :cpu }
+
+ let(:metadata_mock) do
+ Gitlab::Cache::Metadata.new(
+ cache_identifier: cache_identifier,
+ feature_category: feature_category
+ )
+ end
+
+ let(:metrics_mock) { Gitlab::Cache::Metrics.new(metadata_mock) }
+
+ describe '.build_with_metadata' do
+ it 'builds a cache client with metrics support' do
+ attributes = {
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ }
+
+ instance = described_class.build_with_metadata(**attributes)
+
+ expect(instance).to be_a(described_class)
+ expect(instance.metadata).to have_attributes(**attributes)
+ end
+ end
+
+ describe 'Methods', :use_clean_rails_memory_store_caching do
+ let(:expected_key) { 'key' }
+
+ before do
+ allow(Gitlab::Cache::Metrics).to receive(:new).and_return(metrics_mock)
+ end
+
+ describe '#read' do
+ context 'when key does not exist' do
+ it 'returns nil' do
+ expect(client.read('key')).to be_nil
+ end
+
+ it 'increments cache miss' do
+ expect(metrics_mock).to receive(:increment_cache_miss)
+
+ client.read('key')
+ end
+ end
+
+ context 'when key exists' do
+ before do
+ backend.write(expected_key, 'value')
+ end
+
+ it 'returns key value' do
+ expect(client.read('key')).to eq('value')
+ end
+
+ it 'increments cache hit' do
+ expect(metrics_mock).to receive(:increment_cache_hit)
+
+ client.read('key')
+ end
+ end
+ end
+
+ describe '#write' do
+ it 'calls backend "#write" method with the expected key' do
+ expect(backend).to receive(:write).with(expected_key, 'value')
+
+ client.write('key', 'value')
+ end
+ end
+
+ describe '#exist?' do
+ it 'calls backend "#exist?" method with the expected key' do
+ expect(backend).to receive(:exist?).with(expected_key)
+
+ client.exist?('key')
+ end
+ end
+
+ describe '#delete' do
+ it 'calls backend "#delete" method with the expected key' do
+ expect(backend).to receive(:delete).with(expected_key)
+
+ client.delete('key')
+ end
+ end
+
+ # rubocop:disable Style/RedundantFetchBlock
+ describe '#fetch' do
+ it 'creates key in the specific format' do
+ client.fetch('key') { 'value' }
+
+ expect(backend.fetch(expected_key)).to eq('value')
+ end
+
+ it 'yields the block once' do
+ expect { |b| client.fetch('key', &b) }.to yield_control.once
+ end
+
+ context 'when key already exists' do
+ before do
+ backend.write(expected_key, 'value')
+ end
+
+ it 'does not redefine the value' do
+ expect(client.fetch('key') { 'new-value' }).to eq('value')
+ end
+
+ it 'increments a cache hit' do
+ expect(metrics_mock).to receive(:increment_cache_hit)
+
+ client.fetch('key')
+ end
+
+ it 'does not measure the cache generation time' do
+ expect(metrics_mock).not_to receive(:observe_cache_generation)
+
+ client.fetch('key') { 'new-value' }
+ end
+ end
+
+ context 'when key does not exist' do
+ it 'caches the key' do
+ expect(client.fetch('key') { 'value' }).to eq('value')
+
+ expect(client.fetch('key')).to eq('value')
+ end
+
+ it 'increments a cache miss' do
+ expect(metrics_mock).to receive(:increment_cache_miss)
+
+ client.fetch('key')
+ end
+
+ it 'measures the cache generation time' do
+ expect(metrics_mock).to receive(:observe_cache_generation)
+
+ client.fetch('key') { 'value' }
+ end
+ end
+ end
+ end
+ # rubocop:enable Style/RedundantFetchBlock
+end
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
index 762a121340e..77deffe4b37 100644
--- a/spec/lib/gitlab/changes_list_spec.rb
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::ChangesList do
+RSpec.describe Gitlab::ChangesList, feature_category: :source_code_management do
let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" }
let(:invalid_changes) { 1 }
diff --git a/spec/lib/gitlab/chat/responder_spec.rb b/spec/lib/gitlab/chat/responder_spec.rb
index a9d290cb87c..15ca3427ae8 100644
--- a/spec/lib/gitlab/chat/responder_spec.rb
+++ b/spec/lib/gitlab/chat/responder_spec.rb
@@ -4,68 +4,32 @@ require 'spec_helper'
RSpec.describe Gitlab::Chat::Responder, feature_category: :integrations do
describe '.responder_for' do
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(use_response_url_for_chat_responder: false)
- end
-
- context 'using a regular build' do
- it 'returns nil' do
- build = create(:ci_build)
+ context 'using a regular build' do
+ it 'returns nil' do
+ build = create(:ci_build)
- expect(described_class.responder_for(build)).to be_nil
- end
- end
-
- context 'using a chat build' do
- it 'returns the responder for the build' do
- pipeline = create(:ci_pipeline)
- build = create(:ci_build, pipeline: pipeline)
- integration = double(:integration, chat_responder: Gitlab::Chat::Responder::Slack)
- chat_name = double(:chat_name, integration: integration)
- chat_data = double(:chat_data, chat_name: chat_name)
-
- allow(pipeline)
- .to receive(:chat_data)
- .and_return(chat_data)
-
- expect(described_class.responder_for(build))
- .to be_an_instance_of(Gitlab::Chat::Responder::Slack)
- end
+ expect(described_class.responder_for(build)).to be_nil
end
end
- context 'when the feature flag is enabled' do
- before do
- stub_feature_flags(use_response_url_for_chat_responder: true)
- end
-
- context 'using a regular build' do
- it 'returns nil' do
- build = create(:ci_build)
+ context 'using a chat build' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
- expect(described_class.responder_for(build)).to be_nil
+ context "when response_url starts with 'https://hooks.slack.com/'" do
+ before do
+ pipeline.build_chat_data(response_url: 'https://hooks.slack.com/services/12345', chat_name_id: 'U123')
end
+
+ it { expect(described_class.responder_for(build)).to be_an_instance_of(Gitlab::Chat::Responder::Slack) }
end
- context 'using a chat build' do
- let(:chat_name) { create(:chat_name, chat_id: 'U123') }
- let(:pipeline) do
- pipeline = create(:ci_pipeline)
- pipeline.create_chat_data!(
- response_url: 'https://hooks.slack.com/services/12345',
- chat_name_id: chat_name.id
- )
- pipeline
+ context "when response_url does not start with 'https://hooks.slack.com/'" do
+ before do
+ pipeline.build_chat_data(response_url: 'https://mattermost.example.com/services/12345', chat_name_id: 'U123')
end
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:responder) { described_class.new(build) }
-
- it 'returns the responder for the build' do
- expect(described_class.responder_for(build))
- .to be_an_instance_of(Gitlab::Chat::Responder::Slack)
- end
+ it { expect(described_class.responder_for(build)).to be_an_instance_of(Gitlab::Chat::Responder::Mattermost) }
end
end
end
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
index 60118823b5a..552afcdb180 100644
--- a/spec/lib/gitlab/checks/changes_access_spec.rb
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Checks::ChangesAccess do
+RSpec.describe Gitlab::Checks::ChangesAccess, feature_category: :source_code_management do
include_context 'changes access checks context'
subject { changes_access }
@@ -47,6 +47,16 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
expect(subject.commits).to match_array([])
end
+ context 'when change is for notes ref' do
+ let(:changes) do
+ [{ oldrev: oldrev, newrev: newrev, ref: 'refs/notes/commit' }]
+ end
+
+ it 'does not return any commits' do
+ expect(subject.commits).to match_array([])
+ end
+ end
+
context 'when changes contain empty revisions' do
let(:expected_commit) { instance_double(Commit) }
diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb
index 6b45b8d4628..0845c746545 100644
--- a/spec/lib/gitlab/checks/diff_check_spec.rb
+++ b/spec/lib/gitlab/checks/diff_check_spec.rb
@@ -2,10 +2,20 @@
require 'spec_helper'
-RSpec.describe Gitlab::Checks::DiffCheck do
+RSpec.describe Gitlab::Checks::DiffCheck, feature_category: :source_code_management do
include_context 'change access checks context'
describe '#validate!' do
+ context 'when ref is not tag or branch ref' do
+ let(:ref) { 'refs/notes/commit' }
+
+ it 'does not call find_changed_paths' do
+ expect(project.repository).not_to receive(:find_changed_paths)
+
+ subject.validate!
+ end
+ end
+
context 'when commits is empty' do
it 'does not call find_changed_paths' do
expect(project.repository).not_to receive(:find_changed_paths)
diff --git a/spec/lib/gitlab/ci/badge/release/template_spec.rb b/spec/lib/gitlab/ci/badge/release/template_spec.rb
index 2b66c296a94..6be0dcaae99 100644
--- a/spec/lib/gitlab/ci/badge/release/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/release/template_spec.rb
@@ -59,9 +59,30 @@ RSpec.describe Gitlab::Ci::Badge::Release::Template do
end
describe '#value_width' do
- it 'has a fixed value width' do
+ it 'returns the default value width' do
expect(template.value_width).to eq 54
end
+
+ it 'returns custom value width' do
+ value_width = 100
+ badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { value_width: value_width })
+
+ expect(described_class.new(badge).value_width).to eq value_width
+ end
+
+ it 'returns VALUE_WIDTH_DEFAULT if the custom value_width supplied is greater than permissible limit' do
+ value_width = 250
+ badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { value_width: value_width })
+
+ expect(described_class.new(badge).value_width).to eq 54
+ end
+
+ it 'returns VALUE_WIDTH_DEFAULT if value_width is not a number' do
+ value_width = "string"
+ badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { value_width: value_width })
+
+ expect(described_class.new(badge).value_width).to eq 54
+ end
end
describe '#key_color' do
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index 314714c543b..0b275e7d564 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_composition do
let(:auto_retry) { described_class.new(build) }
describe '#allowed?' do
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
index 74739a67be0..4fdeffb033a 100644
--- a/spec/lib/gitlab/ci/build/context/build_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_composition do
let(:pipeline) { create(:ci_pipeline) }
let(:seed_attributes) { { 'name' => 'some-job' } }
diff --git a/spec/lib/gitlab/ci/build/hook_spec.rb b/spec/lib/gitlab/ci/build/hook_spec.rb
index 6ed40a44c97..6c9175b4260 100644
--- a/spec/lib/gitlab/ci/build/hook_spec.rb
+++ b/spec/lib/gitlab/ci/build/hook_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_composition do
let_it_be(:build1) do
FactoryBot.build(:ci_build,
options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
diff --git a/spec/lib/gitlab/ci/components/header_spec.rb b/spec/lib/gitlab/ci/components/header_spec.rb
new file mode 100644
index 00000000000..b1af4ca9238
--- /dev/null
+++ b/spec/lib/gitlab/ci/components/header_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Components::Header, feature_category: :pipeline_composition do
+ subject { described_class.new(spec) }
+
+ context 'when spec is valid' do
+ let(:spec) do
+ {
+ spec: {
+ inputs: {
+ website: nil,
+ run: {
+ options: %w[opt1 opt2]
+ }
+ }
+ }
+ }
+ end
+
+ it 'fabricates a spec from valid data' do
+ expect(subject).not_to be_empty
+ end
+
+ describe '#inputs' do
+ it 'fabricates input data' do
+ input = subject.inputs({ website: 'https//gitlab.com', run: 'opt1' })
+
+ expect(input.count).to eq 2
+ end
+ end
+
+ describe '#context' do
+ it 'fabricates interpolation context' do
+ ctx = subject.context({ website: 'https//gitlab.com', run: 'opt1' })
+
+ expect(ctx).to be_valid
+ end
+ end
+ end
+
+ context 'when spec is empty' do
+ let(:spec) { { spec: {} } }
+
+ it 'returns an empty header' do
+ expect(subject).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb
index d9beae0555c..fbe5e0b9d42 100644
--- a/spec/lib/gitlab/ci/components/instance_path_spec.rb
+++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_composition do
let_it_be(:user) { create(:user) }
let(:path) { described_class.new(address: address, content_filename: 'template.yml') }
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index c1b9bd58d98..c8b4a8b8a0e 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_composition do
let(:entry) { described_class.new(config, name: :rspec) }
it_behaves_like 'with inheritable CI config' do
@@ -728,27 +728,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho
scheduling_type: :stage,
id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
end
-
- context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
- before do
- stub_feature_flags(ci_hooks_pre_get_sources_script: false)
- end
-
- it 'returns correct value' do
- expect(entry.value)
- .to eq(name: :rspec,
- before_script: %w[ls pwd],
- script: %w[rspec],
- stage: 'test',
- ignore: false,
- after_script: %w[cleanup],
- only: { refs: %w[branches tags] },
- job_variables: {},
- root_variables_inheritance: true,
- scheduling_type: :stage,
- id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } })
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 378c0947e8a..7093a0a6edf 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Policy do
+RSpec.describe Gitlab::Ci::Config::Entry::Policy, feature_category: :continuous_integration do
let(:entry) { described_class.new(config) }
context 'when using simplified policy' do
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index b28562ba2ea..4f13940d7e2 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeline_composition do
let(:node_class) do
Class.new(::Gitlab::Config::Entry::Node) do
include Gitlab::Ci::Config::Entry::Processable
diff --git a/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb b/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb
index c35355b10c6..40507a66c2d 100644
--- a/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy do
+RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy, feature_category: :continuous_integration do
let(:entry) { described_class.new(config) }
describe '#value' do
diff --git a/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb b/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb
index ccd6f6ab427..6f37dd72083 100644
--- a/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Reports::CoverageReport, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::Entry::Reports::CoverageReport, feature_category: :pipeline_composition do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 715cb18fb92..73bf2d422b7 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_composition do
let(:entry) { described_class.new(config) }
describe 'validates ALLOWED_KEYS' do
diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
index f47923af45a..fdd598c2ab2 100644
--- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Entry::Trigger, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::Entry::Trigger, feature_category: :pipeline_composition do
subject { described_class.new(config) }
context 'when trigger config is a non-empty string' do
diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb
index 1fd3cf3c99f..fc68d3d62a2 100644
--- a/spec/lib/gitlab/ci/config/external/context_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/context_spec.rb
@@ -2,12 +2,21 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_composition do
let(:project) { build(:project) }
let(:user) { double('User') }
let(:sha) { '12345' }
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'a', 'value' => 'b' }]) }
- let(:attributes) { { project: project, user: user, sha: sha, variables: variables } }
+ let(:pipeline_config) { instance_double(Gitlab::Ci::ProjectConfig) }
+ let(:attributes) do
+ {
+ project: project,
+ user: user,
+ sha: sha,
+ variables: variables,
+ pipeline_config: pipeline_config
+ }
+ end
subject(:subject) { described_class.new(**attributes) }
@@ -15,11 +24,12 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin
context 'with values' do
it { is_expected.to have_attributes(**attributes) }
it { expect(subject.expandset).to eq([]) }
- it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) }
+ it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
it { expect(subject.execution_deadline).to eq(0) }
it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
it { expect(subject.variables_hash).to include('a' => 'b') }
+ it { expect(subject.pipeline_config).to eq(pipeline_config) }
end
context 'without values' do
@@ -27,37 +37,11 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin
it { is_expected.to have_attributes(**attributes) }
it { expect(subject.expandset).to eq([]) }
- it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) }
+ it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
it { expect(subject.execution_deadline).to eq(0) }
it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
- end
-
- context 'when FF ci_includes_count_duplicates is disabled' do
- before do
- stub_feature_flags(ci_includes_count_duplicates: false)
- end
-
- context 'with values' do
- it { is_expected.to have_attributes(**attributes) }
- it { expect(subject.expandset).to eq(Set.new) }
- it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
- it { expect(subject.execution_deadline).to eq(0) }
- it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
- it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
- it { expect(subject.variables_hash).to include('a' => 'b') }
- end
-
- context 'without values' do
- let(:attributes) { { project: nil, user: nil, sha: nil } }
-
- it { is_expected.to have_attributes(**attributes) }
- it { expect(subject.expandset).to eq(Set.new) }
- it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
- it { expect(subject.execution_deadline).to eq(0) }
- it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
- it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
- end
+ it { expect(subject.pipeline_config).to be_nil }
end
end
@@ -170,4 +154,26 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin
describe '#sentry_payload' do
it { expect(subject.sentry_payload).to match(a_hash_including(:project, :user)) }
end
+
+ describe '#internal_include?' do
+ context 'when pipeline_config is provided' do
+ where(:value) { [true, false] }
+
+ with_them do
+ it 'returns the value of .internal_include_prepended?' do
+ allow(pipeline_config).to receive(:internal_include_prepended?).and_return(value)
+
+ expect(subject.internal_include?).to eq(value)
+ end
+ end
+ end
+
+ context 'when pipeline_config is not provided' do
+ let(:pipeline_config) { nil }
+
+ it 'returns false' do
+ expect(subject.internal_include?).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
index 45a15fb5f36..52b8dcbcb44 100644
--- a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :pipeline_composition do
let(:parent_pipeline) { create(:ci_pipeline) }
let(:variables) {}
let(:context) do
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index 55d95d0c1f8..959dcdf31af 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipeline_composition do
let(:variables) {}
let(:context_params) { { sha: 'HEAD', variables: variables } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
index a162a1a8abf..1562e571060 100644
--- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_composition do
let_it_be(:context_project) { create(:project, :repository) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
index b5895b4bc81..2bac8a6968b 100644
--- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pipeline_composition do
include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
index abe38cdbc3e..0ef39a22932 100644
--- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :pipeline_composition do
include RepoHelpers
let_it_be(:context_project) { create(:project) }
@@ -97,6 +97,36 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p
end
end
+ context 'when a valid path is used in uppercase' do
+ let(:params) do
+ { project: project.full_path.upcase, file: '/file.yml' }
+ end
+
+ around do |example|
+ create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do
+ example.run
+ end
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when a valid different case path is used' do
+ let_it_be(:project) { create(:project, :repository, path: 'mY-teSt-proJect', name: 'My Test Project') }
+
+ let(:params) do
+ { project: "#{project.namespace.full_path}/my-test-projecT", file: '/file.yml' }
+ end
+
+ around do |example|
+ create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do
+ example.run
+ end
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
context 'when a valid path with custom ref is used' do
let(:params) do
{ project: project.full_path, ref: 'master', file: '/file.yml' }
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 27f401db76e..f8986e8fa10 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pipeline_composition do
include StubRequests
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) }
diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
index 83e98874118..79fd4203c3e 100644
--- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb
index 0fdcc5e8ff7..ce8f3756cbc 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::Base, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Base, feature_category: :pipeline_composition do
let(:test_class) do
Class.new(described_class) do
def self.name
diff --git a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
index df2a2f0fd01..5195567ebb4 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :pipeline_composition do
let_it_be(:variables) do
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'VARIABLE1', value: 'hello')
diff --git a/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb
index b14b6b0ca29..1e490bf1d16 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::LocationExpander, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::LocationExpander, feature_category: :pipeline_composition do
include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
index 11c79e19cff..6ca4fd24e61 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: :pipeline_composition do
let_it_be(:variables) do
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'A_MASKED_VAR', value: 'this-is-secret', masked: true)
diff --git a/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb
index 709c234253b..09212833d84 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::Normalizer, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Normalizer, feature_category: :pipeline_composition do
let_it_be(:variables) do
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'VARIABLE1', value: 'config')
diff --git a/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb
index f7454dcd4be..e27e8034faa 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::VariablesExpander, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::VariablesExpander, feature_category: :pipeline_composition do
let_it_be(:variables) do
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'VARIABLE1', value: 'hello')
diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
index a219666f24e..ef5049158c1 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: :pipeline_composition do
include RepoHelpers
include StubRequests
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :small_repo) }
let_it_be(:user) { project.owner }
let(:context) do
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
}
end
- around(:all) do |example|
+ around do |example|
create_and_delete_files(project, project_files) do
example.run
end
@@ -84,42 +84,105 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
end
context 'when files are project files' do
- let_it_be(:included_project) { create(:project, :repository, namespace: project.namespace, creator: user) }
+ let_it_be(:included_project1) { create(:project, :small_repo, namespace: project.namespace, creator: user) }
+ let_it_be(:included_project2) { create(:project, :small_repo, namespace: project.namespace, creator: user) }
let(:files) do
[
Gitlab::Ci::Config::External::File::Project.new(
- { file: 'myfolder/file1.yml', project: included_project.full_path }, context
+ { file: 'myfolder/file1.yml', project: included_project1.full_path }, context
),
Gitlab::Ci::Config::External::File::Project.new(
- { file: 'myfolder/file2.yml', project: included_project.full_path }, context
+ { file: 'myfolder/file2.yml', project: included_project1.full_path }, context
),
Gitlab::Ci::Config::External::File::Project.new(
- { file: 'myfolder/file3.yml', project: included_project.full_path }, context
+ { file: 'myfolder/file3.yml', project: included_project1.full_path, ref: 'master' }, context
+ ),
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file1.yml', project: included_project2.full_path }, context
+ ),
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file2.yml', project: included_project2.full_path }, context
)
]
end
- around(:all) do |example|
- create_and_delete_files(included_project, project_files) do
- example.run
+ around do |example|
+ create_and_delete_files(included_project1, project_files) do
+ create_and_delete_files(included_project2, project_files) do
+ example.run
+ end
end
end
- it 'returns an array of file objects' do
+ it 'returns an array of valid file objects' do
expect(process.map(&:location)).to contain_exactly(
- 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml'
+ 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml', 'myfolder/file1.yml', 'myfolder/file2.yml'
)
+
+ expect(process.all?(&:valid?)).to be_truthy
end
it 'adds files to the expandset' do
- expect { process }.to change { context.expandset.count }.by(3)
+ expect { process }.to change { context.expandset.count }.by(5)
end
it 'calls Gitaly only once for all files', :request_store do
- # 1 for project.commit.id, 3 for the sha check, 1 for the files
+ files # calling this to load project creations and the `project.commit.id` call
+
+ # 3 for the sha check, 2 for the files in batch
expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(5)
end
+
+ it 'queries with batch', :use_sql_query_cache do
+ files # calling this to load project creations and the `project.commit.id` call
+
+ queries = ActiveRecord::QueryRecorder.new(skip_cached: false) { process }
+ projects_queries = queries.occurrences_starting_with('SELECT "projects"')
+ access_check_queries = queries.occurrences_starting_with('SELECT MAX("project_authorizations"."access_level")')
+
+ # We could not reduce the number of projects queries because we need to call project for
+ # the `can_access_local_content?` and `sha` BatchLoaders.
+ expect(projects_queries.values.sum).to eq(2)
+ expect(access_check_queries.values.sum).to eq(2)
+ end
+
+ context 'when the FF ci_batch_project_includes_context is disabled' do
+ before do
+ stub_feature_flags(ci_batch_project_includes_context: false)
+ end
+
+ it 'returns an array of file objects' do
+ expect(process.map(&:location)).to contain_exactly(
+ 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml',
+ 'myfolder/file1.yml', 'myfolder/file2.yml'
+ )
+ end
+
+ it 'adds files to the expandset' do
+ expect { process }.to change { context.expandset.count }.by(5)
+ end
+
+ it 'calls Gitaly for all files', :request_store do
+ files # calling this to load project creations and the `project.commit.id` call
+
+ # 5 for the sha check, 2 for the files in batch
+ expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(7)
+ end
+
+ it 'queries without batch', :use_sql_query_cache do
+ files # calling this to load project creations and the `project.commit.id` call
+
+ queries = ActiveRecord::QueryRecorder.new(skip_cached: false) { process }
+ projects_queries = queries.occurrences_starting_with('SELECT "projects"')
+ access_check_queries = queries.occurrences_starting_with(
+ 'SELECT MAX("project_authorizations"."access_level")'
+ )
+
+ expect(projects_queries.values.sum).to eq(5)
+ expect(access_check_queries.values.sum).to eq(5)
+ end
+ end
end
context 'when a file includes other files' do
@@ -150,7 +213,30 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
end
end
- context 'when total file count exceeds max_includes' do
+ describe 'max includes detection' do
+ shared_examples 'verifies max includes' do
+ context 'when total file count is equal to max_includes' do
+ before do
+ allow(context).to receive(:max_includes).and_return(expected_total_file_count)
+ end
+
+ it 'adds the expected number of files to expandset' do
+ expect { process }.not_to raise_error
+ expect(context.expandset.count).to eq(expected_total_file_count)
+ end
+ end
+
+ context 'when total file count exceeds max_includes' do
+ before do
+ allow(context).to receive(:max_includes).and_return(expected_total_file_count - 1)
+ end
+
+ it 'raises error' do
+ expect { process }.to raise_error(expected_error_class)
+ end
+ end
+ end
+
context 'when files are nested' do
let(:files) do
[
@@ -158,9 +244,21 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
]
end
- it 'raises Processor::IncludeError' do
- allow(context).to receive(:max_includes).and_return(1)
- expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError)
+ let(:expected_total_file_count) { 4 } # Includes nested_configs.yml + 3 nested files
+ let(:expected_error_class) { Gitlab::Ci::Config::External::Processor::IncludeError }
+
+ it_behaves_like 'verifies max includes'
+
+ context 'when duplicate files are included' do
+ let(:expected_total_file_count) { 8 } # 2 x (Includes nested_configs.yml + 3 nested files)
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context)
+ ]
+ end
+
+ it_behaves_like 'verifies max includes'
end
end
@@ -172,34 +270,162 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
]
end
- it 'raises Mapper::TooManyIncludesError' do
- allow(context).to receive(:max_includes).and_return(1)
- expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
+ let(:expected_total_file_count) { files.count }
+ let(:expected_error_class) { Gitlab::Ci::Config::External::Mapper::TooManyIncludesError }
+
+ it_behaves_like 'verifies max includes'
+
+ context 'when duplicate files are included' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context)
+ ]
+ end
+
+ let(:expected_total_file_count) { files.count }
+
+ it_behaves_like 'verifies max includes'
end
end
- context 'when files are duplicates' do
+ context 'when there is a circular include' do
+ let(:project_files) do
+ {
+ 'myfolder/file1.yml' => <<~YAML
+ include: myfolder/file1.yml
+ YAML
+ }
+ end
+
let(:files) do
[
- Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
- Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context)
]
end
+ before do
+ allow(context).to receive(:max_includes).and_return(10)
+ end
+
it 'raises error' do
- allow(context).to receive(:max_includes).and_return(2)
- expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError)
end
+ end
- context 'when FF ci_includes_count_duplicates is disabled' do
- before do
- stub_feature_flags(ci_includes_count_duplicates: false)
+ context 'when a file is an internal include' do
+ let(:project_files) do
+ {
+ 'myfolder/file1.yml' => <<~YAML,
+ my_build:
+ script: echo Hello World
+ YAML
+ '.internal-include.yml' => <<~YAML
+ include:
+ - local: myfolder/file1.yml
+ YAML
+ }
+ end
+
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: '.internal-include.yml' }, context)
+ ]
+ end
+
+ let(:total_file_count) { 2 } # Includes .internal-include.yml + myfolder/file1.yml
+ let(:pipeline_config) { instance_double(Gitlab::Ci::ProjectConfig) }
+
+ let(:context) do
+ Gitlab::Ci::Config::External::Context.new(
+ project: project,
+ user: user,
+ sha: project.commit.id,
+ pipeline_config: pipeline_config
+ )
+ end
+
+ before do
+ allow(pipeline_config).to receive(:internal_include_prepended?).and_return(true)
+ allow(context).to receive(:max_includes).and_return(1)
+ end
+
+ context 'when total file count excluding internal include is equal to max_includes' do
+ it 'does not add the internal include to expandset' do
+ expect { process }.not_to raise_error
+ expect(context.expandset.count).to eq(total_file_count - 1)
+ expect(context.expandset.first.location).to eq('myfolder/file1.yml')
end
+ end
- it 'does not raise error' do
+ context 'when total file count excluding internal include exceeds max_includes' do
+ let(:project_files) do
+ {
+ 'myfolder/file1.yml' => <<~YAML,
+ my_build:
+ script: echo Hello World
+ YAML
+ '.internal-include.yml' => <<~YAML
+ include:
+ - local: myfolder/file1.yml
+ - local: myfolder/file1.yml
+ YAML
+ }
+ end
+
+ it 'raises error' do
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError)
+ end
+ end
+ end
+ end
+
+ context 'when FF ci_fix_max_includes is disabled' do
+ before do
+ stub_feature_flags(ci_fix_max_includes: false)
+ end
+
+ context 'when total file count exceeds max_includes' do
+ context 'when files are nested' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context)
+ ]
+ end
+
+ it 'raises Processor::IncludeError' do
+ allow(context).to receive(:max_includes).and_return(1)
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError)
+ end
+ end
+
+ context 'when files are not nested' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context)
+ ]
+ end
+
+ it 'raises Mapper::TooManyIncludesError' do
+ allow(context).to receive(:max_includes).and_return(1)
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
+ end
+ end
+
+ context 'when files are duplicates' do
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context)
+ ]
+ end
+
+ it 'raises error' do
allow(context).to receive(:max_includes).and_return(2)
- expect { process }.not_to raise_error
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
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 b3115617084..56d1ddee4b8 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_composition do
include StubRequests
include RepoHelpers
@@ -234,17 +234,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline
process
expect(context.expandset.size).to eq(2)
end
-
- context 'when FF ci_includes_count_duplicates is disabled' do
- before do
- stub_feature_flags(ci_includes_count_duplicates: false)
- end
-
- it 'has expanset with one' do
- process
- expect(context.expandset.size).to eq(1)
- end
- end
end
context 'when passing max number of files' do
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index bb65c2ef10c..97f600baf25 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipeline_composition do
include StubRequests
include RepoHelpers
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
index 227b62d8ce8..cc73338b5a8 100644
--- a/spec/lib/gitlab/ci/config/external/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do
let(:rule_hashes) {}
subject(:rules) { described_class.new(rule_hashes) }
diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb
new file mode 100644
index 00000000000..73b5b8f9497
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/header/input_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_composition do
+ let(:factory) do
+ Gitlab::Config::Entry::Factory
+ .new(described_class)
+ .value(input_hash)
+ .with(key: input_name)
+ end
+
+ let(:input_name) { 'foo' }
+
+ subject(:config) { factory.create!.tap(&:compose!) }
+
+ shared_examples 'a valid input' do
+ let(:expected_hash) { input_hash }
+
+ it 'passes validations' do
+ expect(config).to be_valid
+ expect(config.errors).to be_empty
+ end
+
+ it 'returns the value' do
+ expect(config.value).to eq(expected_hash)
+ end
+ end
+
+ shared_examples 'an invalid input' do
+ let(:expected_hash) { input_hash }
+
+ it 'fails validations' do
+ expect(config).not_to be_valid
+ expect(config.errors).to eq(expected_errors)
+ end
+
+ it 'returns the value' do
+ expect(config.value).to eq(expected_hash)
+ end
+ end
+
+ context 'when has a default value' do
+ let(:input_hash) { { default: 'bar' } }
+
+ it_behaves_like 'a valid input'
+ end
+
+ context 'when is a required required input' do
+ let(:input_hash) { nil }
+
+ it_behaves_like 'a valid input'
+ end
+
+ context 'when contains unknown keywords' do
+ let(:input_hash) { { test: 123 } }
+ let(:expected_errors) { ['foo config contains unknown keys: test'] }
+
+ it_behaves_like 'an invalid input'
+ end
+
+ context 'when has invalid name' do
+ let(:input_name) { [123] }
+ let(:input_hash) { {} }
+
+ let(:expected_errors) { ['123 key must be an alphanumeric string'] }
+
+ it_behaves_like 'an invalid input'
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/header/root_spec.rb b/spec/lib/gitlab/ci/config/header/root_spec.rb
new file mode 100644
index 00000000000..55f77137619
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/header/root_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Header::Root, feature_category: :pipeline_composition do
+ let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(header_hash) }
+
+ subject(:config) { factory.create!.tap(&:compose!) }
+
+ shared_examples 'a valid header' do
+ let(:expected_hash) { header_hash }
+
+ it 'passes validations' do
+ expect(config).to be_valid
+ expect(config.errors).to be_empty
+ end
+
+ it 'returns the value' do
+ expect(config.value).to eq(expected_hash)
+ end
+ end
+
+ shared_examples 'an invalid header' do
+ let(:expected_hash) { header_hash }
+
+ it 'fails validations' do
+ expect(config).not_to be_valid
+ expect(config.errors).to eq(expected_errors)
+ end
+
+ it 'returns the value' do
+ expect(config.value).to eq(expected_hash)
+ end
+ end
+
+ context 'when header contains default and required values for inputs' do
+ let(:header_hash) do
+ {
+ spec: {
+ inputs: {
+ test: {},
+ foo: {
+ default: 'bar'
+ }
+ }
+ }
+ }
+ end
+
+ it_behaves_like 'a valid header'
+ end
+
+ context 'when header contains minimal data' do
+ let(:header_hash) do
+ {
+ spec: {
+ inputs: nil
+ }
+ }
+ end
+
+ it_behaves_like 'a valid header' do
+ let(:expected_hash) { { spec: {} } }
+ end
+ end
+
+ context 'when header contains required inputs' do
+ let(:header_hash) do
+ {
+ spec: {
+ inputs: { foo: nil }
+ }
+ }
+ end
+
+ it_behaves_like 'a valid header' do
+ let(:expected_hash) do
+ {
+ spec: {
+ inputs: { foo: {} }
+ }
+ }
+ end
+ end
+ end
+
+ context 'when header contains unknown keywords' do
+ let(:header_hash) { { test: 123 } }
+ let(:expected_errors) { ['root config contains unknown keys: test'] }
+
+ it_behaves_like 'an invalid header'
+ end
+
+ context 'when header input entry has an unknown key' do
+ let(:header_hash) do
+ {
+ spec: {
+ inputs: {
+ foo: {
+ bad: 'value'
+ }
+ }
+ }
+ }
+ end
+
+ let(:expected_errors) { ['spec:inputs:foo config contains unknown keys: bad'] }
+
+ it_behaves_like 'an invalid header'
+ end
+
+ describe '#inputs_value' do
+ let(:header_hash) do
+ {
+ spec: {
+ inputs: {
+ foo: nil,
+ bar: {
+ default: 'baz'
+ }
+ }
+ }
+ }
+ end
+
+ it 'returns the inputs' do
+ expect(config.inputs_value).to eq({
+ foo: {},
+ bar: { default: 'baz' }
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/header/spec_spec.rb b/spec/lib/gitlab/ci/config/header/spec_spec.rb
new file mode 100644
index 00000000000..cb4237f84ce
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/header/spec_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Header::Spec, feature_category: :pipeline_composition do
+ let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(spec_hash) }
+
+ subject(:config) { factory.create!.tap(&:compose!) }
+
+ context 'when spec contains default values for inputs' do
+ let(:spec_hash) do
+ {
+ inputs: {
+ foo: {
+ default: 'bar'
+ }
+ }
+ }
+ end
+
+ it 'passes validations' do
+ expect(config).to be_valid
+ expect(config.errors).to be_empty
+ end
+
+ it 'returns the value' do
+ expect(config.value).to eq(spec_hash)
+ end
+ end
+
+ context 'when spec contains unknown keywords' do
+ let(:spec_hash) { { test: 123 } }
+ let(:expected_errors) { ['spec config contains unknown keys: test'] }
+
+ it 'fails validations' do
+ expect(config).not_to be_valid
+ expect(config.errors).to eq(expected_errors)
+ end
+
+ it 'returns the value' do
+ expect(config.value).to eq(spec_hash)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb
new file mode 100644
index 00000000000..eda15ee9eb2
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_composition do
+ it 'does not have a header when config is a single hash' do
+ result = described_class.new(config: { a: 1, b: 2 })
+
+ expect(result).not_to have_header
+ end
+
+ it 'has a header when config is an array of hashes' do
+ result = described_class.new(config: [{ a: 1 }, { b: 2 }])
+
+ expect(result).to have_header
+ expect(result.header).to eq({ a: 1 })
+ end
+
+ it 'raises an error when reading a header when there is none' do
+ result = described_class.new(config: { b: 2 })
+
+ expect { result.header }.to raise_error(ArgumentError)
+ end
+
+ it 'stores an error / exception when initialized with it' do
+ result = described_class.new(error: ArgumentError.new('abc'))
+
+ expect(result).not_to be_valid
+ expect(result.error).to be_a ArgumentError
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb
index 4b34553f55e..f4b70069bbe 100644
--- a/spec/lib/gitlab/ci/config/yaml_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do
describe '.load!' do
it 'loads a single-doc YAML file' do
yaml = <<~YAML
@@ -50,6 +50,15 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring d
})
end
+ context 'when YAML is invalid' do
+ let(:yaml) { 'some: invalid: syntax' }
+
+ it 'raises an error' do
+ expect { described_class.load!(yaml) }
+ .to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/
+ end
+ end
+
context 'when ci_multi_doc_yaml is disabled' do
before do
stub_feature_flags(ci_multi_doc_yaml: false)
@@ -102,4 +111,38 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring d
end
end
end
+
+ describe '.load_result!' do
+ context 'when syntax is invalid' do
+ let(:yaml) { 'some: invalid: syntax' }
+
+ it 'returns an invalid result object' do
+ result = described_class.load_result!(yaml)
+
+ expect(result).not_to be_valid
+ expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
+ end
+ end
+
+ context 'when syntax is valid and contains a header document' do
+ let(:yaml) do
+ <<~YAML
+ a: 1
+ ---
+ b: 2
+ YAML
+ end
+
+ let(:project) { create(:project) }
+
+ it 'returns a result object' do
+ result = described_class.load_result!(yaml, project: project)
+
+ expect(result).to be_valid
+ expect(result.error).to be_nil
+ expect(result.header).to eq({ a: 1 })
+ expect(result.content).to eq({ b: 2 })
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 5cdc9c21561..fdf152b3584 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_composition do
include StubRequests
include RepoHelpers
diff --git a/spec/lib/gitlab/ci/input/arguments/base_spec.rb b/spec/lib/gitlab/ci/input/arguments/base_spec.rb
new file mode 100644
index 00000000000..ed8e99b7257
--- /dev/null
+++ b/spec/lib/gitlab/ci/input/arguments/base_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Input::Arguments::Base, feature_category: :pipeline_composition do
+ subject do
+ Class.new(described_class) do
+ def validate!; end
+ def to_value; end
+ end
+ end
+
+ it 'fabricates an invalid input argument if unknown value is provided' do
+ argument = subject.new(:something, { spec: 123 }, [:a, :b])
+
+ expect(argument).not_to be_valid
+ expect(argument.errors.first).to eq 'unsupported value in input argument `something`'
+ end
+end
diff --git a/spec/lib/gitlab/ci/input/arguments/default_spec.rb b/spec/lib/gitlab/ci/input/arguments/default_spec.rb
new file mode 100644
index 00000000000..6b5dd441eb7
--- /dev/null
+++ b/spec/lib/gitlab/ci/input/arguments/default_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Input::Arguments::Default, feature_category: :pipeline_composition do
+ it 'returns a user-provided value if it is present' do
+ argument = described_class.new(:website, { default: 'https://gitlab.com' }, 'https://example.gitlab.com')
+
+ expect(argument).to be_valid
+ expect(argument.to_value).to eq 'https://example.gitlab.com'
+ expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' })
+ end
+
+ it 'returns an empty value if user-provider input is empty' do
+ argument = described_class.new(:website, { default: 'https://gitlab.com' }, '')
+
+ expect(argument).to be_valid
+ expect(argument.to_value).to eq ''
+ expect(argument.to_hash).to eq({ website: '' })
+ end
+
+ it 'returns a default value if user-provider one is unknown' do
+ argument = described_class.new(:website, { default: 'https://gitlab.com' }, nil)
+
+ expect(argument).to be_valid
+ expect(argument.to_value).to eq 'https://gitlab.com'
+ expect(argument.to_hash).to eq({ website: 'https://gitlab.com' })
+ end
+
+ it 'returns an error if the argument has not been fabricated correctly' do
+ argument = described_class.new(:website, { required: 'https://gitlab.com' }, 'https://example.gitlab.com')
+
+ expect(argument).not_to be_valid
+ end
+
+ describe '.matches?' do
+ it 'matches specs with default configuration' do
+ expect(described_class.matches?({ default: 'abc' })).to be true
+ end
+
+ it 'does not match specs different configuration keyword' do
+ expect(described_class.matches?({ options: %w[a b] })).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/input/arguments/options_spec.rb b/spec/lib/gitlab/ci/input/arguments/options_spec.rb
new file mode 100644
index 00000000000..afa279ad48d
--- /dev/null
+++ b/spec/lib/gitlab/ci/input/arguments/options_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Input::Arguments::Options, feature_category: :pipeline_composition do
+ it 'returns a user-provided value if it is an allowed one' do
+ argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt1')
+
+ expect(argument).to be_valid
+ expect(argument.to_value).to eq 'opt1'
+ expect(argument.to_hash).to eq({ run: 'opt1' })
+ end
+
+ it 'returns an error if user-provided value is not allowlisted' do
+ argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt3')
+
+ expect(argument).not_to be_valid
+ expect(argument.errors.first).to eq '`run` input: argument value opt3 not allowlisted'
+ end
+
+ it 'returns an error if specification is not correct' do
+ argument = described_class.new(:website, { options: nil }, 'opt1')
+
+ expect(argument).not_to be_valid
+ expect(argument.errors.first).to eq '`website` input: argument specification invalid'
+ end
+
+ it 'returns an error if specification is using a hash' do
+ argument = described_class.new(:website, { options: { a: 1 } }, 'opt1')
+
+ expect(argument).not_to be_valid
+ expect(argument.errors.first).to eq '`website` input: argument value opt1 not allowlisted'
+ end
+
+ it 'returns an empty value if it is allowlisted' do
+ argument = described_class.new(:run, { options: ['opt1', ''] }, '')
+
+ expect(argument).to be_valid
+ expect(argument.to_value).to be_empty
+ expect(argument.to_hash).to eq({ run: '' })
+ end
+
+ describe '.matches?' do
+ it 'matches specs with options configuration' do
+ expect(described_class.matches?({ options: %w[a b] })).to be true
+ end
+
+ it 'does not match specs different configuration keyword' do
+ expect(described_class.matches?({ default: 'abc' })).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/input/arguments/required_spec.rb b/spec/lib/gitlab/ci/input/arguments/required_spec.rb
new file mode 100644
index 00000000000..0c2ffc282ea
--- /dev/null
+++ b/spec/lib/gitlab/ci/input/arguments/required_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Input::Arguments::Required, feature_category: :pipeline_composition do
+ it 'returns a user-provided value if it is present' do
+ argument = described_class.new(:website, nil, 'https://example.gitlab.com')
+
+ expect(argument).to be_valid
+ expect(argument.to_value).to eq 'https://example.gitlab.com'
+ expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' })
+ end
+
+ it 'returns an empty value if user-provider value is empty' do
+ argument = described_class.new(:website, nil, '')
+
+ expect(argument).to be_valid
+ expect(argument.to_hash).to eq(website: '')
+ end
+
+ it 'returns an error if user-provided value is unspecified' do
+ argument = described_class.new(:website, nil, nil)
+
+ expect(argument).not_to be_valid
+ expect(argument.errors.first).to eq '`website` input: required value has not been provided'
+ end
+
+ describe '.matches?' do
+ it 'matches specs without configuration' do
+ expect(described_class.matches?(nil)).to be true
+ end
+
+ it 'matches specs with empty configuration' do
+ expect(described_class.matches?('')).to be true
+ end
+
+ it 'does not match specs with configuration' do
+ expect(described_class.matches?({ options: %w[a b] })).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb b/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb
new file mode 100644
index 00000000000..1270423ac72
--- /dev/null
+++ b/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Input::Arguments::Unknown, feature_category: :pipeline_composition do
+ it 'raises an error when someone tries to evaluate the value' do
+ argument = described_class.new(:website, nil, 'https://example.gitlab.com')
+
+ expect(argument).not_to be_valid
+ expect { argument.to_value }.to raise_error ArgumentError
+ end
+
+ describe '.matches?' do
+ it 'always matches' do
+ expect(described_class.matches?('abc')).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/input/inputs_spec.rb b/spec/lib/gitlab/ci/input/inputs_spec.rb
new file mode 100644
index 00000000000..5d2d5192299
--- /dev/null
+++ b/spec/lib/gitlab/ci/input/inputs_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Input::Inputs, feature_category: :pipeline_composition do
+ describe '#valid?' do
+ let(:spec) { { website: nil } }
+
+ it 'describes user-provided inputs' do
+ inputs = described_class.new(spec, { website: 'http://example.gitlab.com' })
+
+ expect(inputs).to be_valid
+ end
+ end
+
+ context 'when proper specification has been provided' do
+ let(:spec) do
+ {
+ website: nil,
+ env: { default: 'development' },
+ run: { options: %w[tests spec e2e] }
+ }
+ end
+
+ let(:args) { { website: 'https://gitlab.com', run: 'tests' } }
+
+ it 'fabricates desired input arguments' do
+ inputs = described_class.new(spec, args)
+
+ expect(inputs).to be_valid
+ expect(inputs.count).to eq 3
+ expect(inputs.to_hash).to eq(args.merge(env: 'development'))
+ end
+ end
+
+ context 'when inputs and args are empty' do
+ it 'is a valid use-case' do
+ inputs = described_class.new({}, {})
+
+ expect(inputs).to be_valid
+ expect(inputs.to_hash).to be_empty
+ end
+ end
+
+ context 'when there are arguments recoincilation errors present' do
+ context 'when required argument is missing' do
+ let(:spec) { { website: nil } }
+
+ it 'returns an error' do
+ inputs = described_class.new(spec, {})
+
+ expect(inputs).not_to be_valid
+ expect(inputs.errors.first).to eq '`website` input: required value has not been provided'
+ end
+ end
+
+ context 'when argument is not present but configured as allowlist' do
+ let(:spec) do
+ { run: { options: %w[opt1 opt2] } }
+ end
+
+ it 'returns an error' do
+ inputs = described_class.new(spec, {})
+
+ expect(inputs).not_to be_valid
+ expect(inputs.errors.first).to eq '`run` input: argument not provided'
+ end
+ end
+ end
+
+ context 'when unknown specification argument has been used' do
+ let(:spec) do
+ {
+ website: nil,
+ env: { default: 'development' },
+ run: { options: %w[tests spec e2e] },
+ test: { unknown: 'something' }
+ }
+ end
+
+ let(:args) { { website: 'https://gitlab.com', run: 'tests' } }
+
+ it 'fabricates an unknown argument entry and returns an error' do
+ inputs = described_class.new(spec, args)
+
+ expect(inputs).not_to be_valid
+ expect(inputs.count).to eq 4
+ expect(inputs.errors.first).to eq '`test` input: unrecognized input argument specification: `unknown`'
+ end
+ end
+
+ context 'when unknown arguments are being passed by a user' do
+ let(:spec) do
+ { env: { default: 'development' } }
+ end
+
+ let(:args) { { website: 'https://gitlab.com', run: 'tests' } }
+
+ it 'returns an error with a list of unknown arguments' do
+ inputs = described_class.new(spec, args)
+
+ expect(inputs).not_to be_valid
+ expect(inputs.errors.first).to eq 'unknown input arguments: [:website, :run]'
+ end
+ end
+
+ context 'when composite specification is being used' do
+ let(:spec) do
+ {
+ env: {
+ default: 'dev',
+ options: %w[test dev prod]
+ }
+ }
+ end
+
+ let(:args) { { env: 'dev' } }
+
+ it 'returns an error describing an unknown specification' do
+ inputs = described_class.new(spec, args)
+
+ expect(inputs).not_to be_valid
+ expect(inputs.errors.first).to eq '`env` input: unrecognized input argument definition'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/interpolation/access_spec.rb
index 9f6108a328d..f327377b7e3 100644
--- a/spec/lib/gitlab/ci/interpolation/access_spec.rb
+++ b/spec/lib/gitlab/ci/interpolation/access_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_composition do
subject { described_class.new(access, ctx) }
let(:access) do
diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb
index 7f2be505d17..4a8709df3dc 100644
--- a/spec/lib/gitlab/ci/interpolation/block_spec.rb
+++ b/spec/lib/gitlab/ci/interpolation/block_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_composition do
subject { described_class.new(block, data, ctx) }
let(:data) do
diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb
index e5987776e00..e745269d8c0 100644
--- a/spec/lib/gitlab/ci/interpolation/config_spec.rb
+++ b/spec/lib/gitlab/ci/interpolation/config_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_composition do
subject { described_class.new(YAML.safe_load(config)) }
let(:config) do
diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/interpolation/context_spec.rb
index ada896f4980..2b126f4a8b3 100644
--- a/spec/lib/gitlab/ci/interpolation/context_spec.rb
+++ b/spec/lib/gitlab/ci/interpolation/context_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_composition do
subject { described_class.new(ctx) }
let(:ctx) do
diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/interpolation/template_spec.rb
index 8a243b4db05..a3ef1bb4445 100644
--- a/spec/lib/gitlab/ci/interpolation/template_spec.rb
+++ b/spec/lib/gitlab/ci/interpolation/template_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_composition do
subject { described_class.new(YAML.safe_load(config), ctx) }
let(:config) do
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index b836ca395fa..b238e9161eb 100644
--- a/spec/lib/gitlab/ci/lint_spec.rb
+++ b/spec/lib/gitlab/ci/lint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -100,8 +100,8 @@ RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_authoring do
end
it 'sets merged_config' do
- root_config = YAML.safe_load(content, [Symbol])
- included_config = YAML.safe_load(included_content, [Symbol])
+ root_config = YAML.safe_load(content, permitted_classes: [Symbol])
+ included_config = YAML.safe_load(included_content, permitted_classes: [Symbol])
expected_config = included_config.merge(root_config).except(:include).deep_stringify_keys
expect(subject.merged_yaml).to eq(expected_config.to_yaml)
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index 5d2d22c04fc..421aa29f860 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Parsers::Security::Common do
+RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnerability_management do
describe '#parse!' do
let_it_be(:scanner_data) do
{
@@ -410,6 +410,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end
end
+ describe 'setting the `found_by_pipeline` attribute' do
+ subject { report.findings.map(&:found_by_pipeline).uniq }
+
+ it { is_expected.to eq([pipeline]) }
+ end
+
describe 'parsing tracking' do
let(:finding) { report.findings.first }
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..a9a52972294 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: :continuous_integration do
let(:project) { create(:project, ci_config_path: ci_config_path) }
let(:pipeline) { build(:ci_pipeline, project: project) }
let(:content) { nil }
@@ -26,6 +26,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'bridge_source'
expect(command.config_content).to eq 'the-yaml'
+ expect(command.pipeline_config.internal_include_prepended?).to eq(false)
end
end
@@ -52,6 +53,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'repository_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
@@ -71,6 +73,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'remote_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
@@ -91,6 +94,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'external_project_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
context 'when path specifies a refname' do
@@ -111,6 +115,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'external_project_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
end
@@ -138,6 +143,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'repository_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
@@ -161,6 +167,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'auto_devops_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
@@ -181,6 +188,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq 'parameter_source'
expect(pipeline.pipeline_config.content).to eq(content)
expect(command.config_content).to eq(content)
+ expect(command.pipeline_config.internal_include_prepended?).to eq(false)
end
end
@@ -197,6 +205,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
expect(pipeline.config_source).to eq('unknown_source')
expect(pipeline.pipeline_config).to be_nil
expect(command.config_content).to be_nil
+ expect(command.pipeline_config).to be_nil
expect(pipeline.errors.full_messages).to include('Missing CI config file')
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
index 36714413da6..89c0ce46237 100644
--- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Duration do
+RSpec.describe Gitlab::Ci::Pipeline::Duration, feature_category: :continuous_integration do
describe '.from_periods' do
let(:calculated_duration) { calculate(data) }
@@ -113,16 +113,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
described_class::Period.new(first, last)
end
- described_class.from_periods(periods.sort_by(&:first))
+ described_class.send(:from_periods, periods.sort_by(&:first))
end
end
describe '.from_pipeline' do
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }
+
let_it_be(:start_time) { Time.current.change(usec: 0) }
let_it_be(:current) { start_time + 1000 }
- let_it_be(:pipeline) { create(:ci_pipeline) }
- let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 60) }
- let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 120) }
+ let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 50) }
+ let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 110) }
let_it_be(:canceled_build) { create_build(:canceled, started_at: start_time + 120, finished_at: start_time + 180) }
let_it_be(:skipped_build) { create_build(:skipped, started_at: start_time) }
let_it_be(:pending_build) { create_build(:pending) }
@@ -141,21 +142,55 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
end
context 'when there is no running build' do
- let(:running_build) { nil }
+ let!(:running_build) { nil }
it 'returns the duration for all the builds' do
travel_to(current) do
- expect(described_class.from_pipeline(pipeline)).to eq 180.seconds
+ # 160 = success (50) + failed (50) + canceled (60)
+ expect(described_class.from_pipeline(pipeline)).to eq 160.seconds
end
end
end
- context 'when there are bridge jobs' do
- let!(:success_bridge) { create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280) }
- let!(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) }
- let!(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) }
- let!(:created_bridge) { create_bridge(:created) }
- let!(:manual_bridge) { create_bridge(:manual) }
+ context 'when there are direct bridge jobs' do
+ let_it_be(:success_bridge) do
+ create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280)
+ end
+
+ let_it_be(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) }
+ # NOTE: bridge won't be `canceled` as it will be marked as failed when downstream pipeline is canceled
+ # @see Ci::Bridge#inherit_status_from_downstream
+ let_it_be(:canceled_bridge) do
+ create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 210)
+ end
+
+ let_it_be(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) }
+ let_it_be(:created_bridge) { create_bridge(:created) }
+ let_it_be(:manual_bridge) { create_bridge(:manual) }
+
+ let_it_be(:success_bridge_pipeline) do
+ create(:ci_pipeline, :success, started_at: start_time + 230, finished_at: start_time + 280).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_bridge, pipeline: p)
+ create_build(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 280)
+ create_bridge(:success, pipeline: p, started_at: start_time + 240, finished_at: start_time + 280)
+ end
+ end
+
+ let_it_be(:failed_bridge_pipeline) do
+ create(:ci_pipeline, :failed, started_at: start_time + 225, finished_at: start_time + 240).tap do |p|
+ create(:ci_sources_pipeline, source_job: failed_bridge, pipeline: p)
+ create_build(:failed, pipeline: p, started_at: start_time + 230, finished_at: start_time + 240)
+ create_bridge(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 240)
+ end
+ end
+
+ let_it_be(:canceled_bridge_pipeline) do
+ create(:ci_pipeline, :canceled, started_at: start_time + 190, finished_at: start_time + 210).tap do |p|
+ create(:ci_sources_pipeline, source_job: canceled_bridge, pipeline: p)
+ create_build(:canceled, pipeline: p, started_at: start_time + 200, finished_at: start_time + 210)
+ create_bridge(:success, pipeline: p, started_at: start_time + 205, finished_at: start_time + 210)
+ end
+ end
it 'returns the duration of the running build' do
travel_to(current) do
@@ -166,12 +201,99 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
context 'when there is no running build' do
let!(:running_build) { nil }
- it 'returns the duration for all the builds and bridge jobs' do
+ it 'returns the duration for all the builds (including self and downstreams)' do
travel_to(current) do
- expect(described_class.from_pipeline(pipeline)).to eq 280.seconds
+ # 220 = 160 (see above)
+ # + success build (45) + failed (10) + canceled (10) - overlapping (success & failed) (5)
+ expect(described_class.from_pipeline(pipeline)).to eq 220.seconds
end
end
end
+
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
+ context 'when there are downstream bridge jobs' do
+ let_it_be(:success_direct_bridge) do
+ create_bridge(:success, started_at: start_time + 280, finished_at: start_time + 400)
+ end
+
+ let_it_be(:success_downstream_pipeline) do
+ create(:ci_pipeline, :success, started_at: start_time + 285, finished_at: start_time + 300).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:success, pipeline: p, started_at: start_time + 290, finished_at: start_time + 296)
+ create_bridge(:success, pipeline: p, started_at: start_time + 285, finished_at: start_time + 288)
+ end
+ end
+
+ let_it_be(:failed_downstream_pipeline) do
+ create(:ci_pipeline, :failed, started_at: start_time + 305, finished_at: start_time + 350).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:failed, pipeline: p, started_at: start_time + 320, finished_at: start_time + 327)
+ create_bridge(:success, pipeline: p, started_at: start_time + 305, finished_at: start_time + 350)
+ end
+ end
+
+ let_it_be(:canceled_downstream_pipeline) do
+ create(:ci_pipeline, :canceled, started_at: start_time + 360, finished_at: start_time + 400).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:canceled, pipeline: p, started_at: start_time + 390, finished_at: start_time + 398)
+ create_bridge(:success, pipeline: p, started_at: start_time + 360, finished_at: start_time + 378)
+ end
+ end
+
+ it 'returns the duration of the running build' do
+ travel_to(current) do
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for all the builds (including self and downstreams)' do
+ travel_to(current) do
+ # 241 = 220 (see above)
+ # + success downstream build (6) + failed (7) + canceled (8)
+ expect(described_class.from_pipeline(pipeline)).to eq 241.seconds
+ end
+ end
+ end
+ end
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
+ end
+
+ it 'does not generate N+1 queries if more builds are added' do
+ travel_to(current) do
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+
+ create_list(:ci_build, 2, :success, pipeline: pipeline, started_at: start_time, finished_at: start_time + 50)
+
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ it 'does not generate N+1 queries if more bridges and their pipeline builds are added' do
+ travel_to(current) do
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+
+ create_list(
+ :ci_bridge, 2, :success,
+ pipeline: pipeline, started_at: start_time + 220, finished_at: start_time + 280).each do |bridge|
+ create(:ci_pipeline, :success, started_at: start_time + 235, finished_at: start_time + 280).tap do |p|
+ create(:ci_sources_pipeline, source_job: bridge, pipeline: p)
+ create_builds(3, :success)
+ end
+ end
+
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+ end
end
private
@@ -180,6 +302,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
create(:ci_build, trait, pipeline: pipeline, **opts)
end
+ def create_builds(counts, trait, **opts)
+ create_list(:ci_build, counts, trait, pipeline: pipeline, **opts)
+ end
+
def create_bridge(trait, **opts)
create(:ci_bridge, trait, pipeline: pipeline, **opts)
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 3043d7f5381..ce68e741d00 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_composition do
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:head_sha) { project.repository.head_commit.id }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index 288ac3f3854..ae40626510f 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage, feature_category: :pipeline_composition do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:previous_stages) { [] }
diff --git a/spec/lib/gitlab/ci/project_config/repository_spec.rb b/spec/lib/gitlab/ci/project_config/repository_spec.rb
index 2105b691d9e..e8a997a7e43 100644
--- a/spec/lib/gitlab/ci/project_config/repository_spec.rb
+++ b/spec/lib/gitlab/ci/project_config/repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::ProjectConfig::Repository do
+RSpec.describe Gitlab::Ci::ProjectConfig::Repository, feature_category: :continuous_integration do
let(:project) { create(:project, :custom_repo, files: files) }
let(:sha) { project.repository.head_commit.sha }
let(:files) { { 'README.md' => 'hello' } }
@@ -44,4 +44,10 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Repository do
it { is_expected.to eq(:repository_source) }
end
+
+ describe '#internal_include_prepended?' do
+ subject { config.internal_include_prepended? }
+
+ it { is_expected.to eq(true) }
+ end
end
diff --git a/spec/lib/gitlab/ci/project_config/source_spec.rb b/spec/lib/gitlab/ci/project_config/source_spec.rb
index dda5c7cdce8..eefabe1babb 100644
--- a/spec/lib/gitlab/ci/project_config/source_spec.rb
+++ b/spec/lib/gitlab/ci/project_config/source_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::ProjectConfig::Source do
+RSpec.describe Gitlab::Ci::ProjectConfig::Source, feature_category: :continuous_integration do
let_it_be(:custom_config_class) { Class.new(described_class) }
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:sha) { '123456' }
@@ -20,4 +20,10 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Source do
it { expect { source }.to raise_error(NotImplementedError) }
end
+
+ describe '#internal_include_prepended?' do
+ subject(:internal_include_prepended) { custom_config.internal_include_prepended? }
+
+ it { expect(internal_include_prepended).to eq(false) }
+ end
end
diff --git a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
deleted file mode 100644
index 6f75e2c55e8..00000000000
--- a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
- let(:identifier) { build(:ci_reports_security_identifier) }
-
- let_it_be(:project) { create(:project, :repository) }
-
- let(:location_param) { build(:ci_reports_security_locations_sast, :dynamic) }
- let(:vulnerability_params) { vuln_params(project.id, [identifier], confidence: :low, severity: :critical) }
- let(:base_vulnerability) { build(:ci_reports_security_finding, location: location_param, **vulnerability_params) }
- let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability]) }
-
- let(:head_vulnerability) { build(:ci_reports_security_finding, location: location_param, uuid: base_vulnerability.uuid, **vulnerability_params) }
- let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability]) }
-
- shared_context 'comparing reports' do
- let(:vul_params) { vuln_params(project.id, [identifier]) }
- let(:base_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
- let(:head_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
- let(:head_vul_findings) { [head_vulnerability, vuln] }
- end
-
- subject { described_class.new(project, base_report, head_report) }
-
- where(vulnerability_finding_signatures: [true, false])
-
- with_them do
- before do
- stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_signatures)
- end
-
- describe '#base_report_out_of_date' do
- context 'no base report' do
- let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
-
- it 'is not out of date' do
- expect(subject.base_report_out_of_date).to be false
- end
- end
-
- context 'base report older than one week' do
- let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) }
- let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) }
-
- it 'is not out of date' do
- expect(subject.base_report_out_of_date).to be true
- end
- end
-
- context 'base report less than one week old' do
- let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) }
- let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) }
-
- it 'is not out of date' do
- expect(subject.base_report_out_of_date).to be false
- end
- end
- end
-
- describe '#added' do
- let(:new_location) { build(:ci_reports_security_locations_sast, :dynamic) }
- let(:vul_params) { vuln_params(project.id, [identifier], confidence: :high) }
- let(:vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:critical], location: new_location, **vul_params) }
- let(:low_vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:low], location: new_location, **vul_params) }
-
- context 'with new vulnerability' do
- let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln]) }
-
- it 'points to source tree' do
- expect(subject.added).to eq([vuln])
- end
- end
-
- context 'when comparing reports with different fingerprints' do
- include_context 'comparing reports'
-
- let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings) }
-
- it 'does not find any overlap' do
- expect(subject.added).to eq(head_vul_findings)
- end
- end
-
- context 'order' do
- let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln, low_vuln]) }
-
- it 'does not change' do
- expect(subject.added).to eq([vuln, low_vuln])
- end
- end
- end
-
- describe '#fixed' do
- let(:vul_params) { vuln_params(project.id, [identifier]) }
- let(:vuln) { build(:ci_reports_security_finding, :dynamic, **vul_params ) }
- let(:medium_vuln) { build(:ci_reports_security_finding, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid, **vul_params) }
-
- context 'with fixed vulnerability' do
- let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
-
- it 'points to base tree' do
- expect(subject.fixed).to eq([vuln])
- end
- end
-
- context 'when comparing reports with different fingerprints' do
- include_context 'comparing reports'
-
- let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
-
- it 'does not find any overlap' do
- expect(subject.fixed).to eq([base_vulnerability, vuln])
- end
- end
-
- context 'order' do
- let(:vul_findings) { [vuln, medium_vuln] }
- let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [*vul_findings, base_vulnerability]) }
-
- it 'does not change' do
- expect(subject.fixed).to eq(vul_findings)
- end
- end
- end
-
- describe 'with empty vulnerabilities' do
- let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
-
- it 'returns empty array when reports are not present' do
- comparer = described_class.new(project, empty_report, empty_report)
-
- expect(comparer.fixed).to eq([])
- expect(comparer.added).to eq([])
- end
-
- it 'returns added vulnerability when base is empty and head is not empty' do
- comparer = described_class.new(project, empty_report, head_report)
-
- expect(comparer.fixed).to eq([])
- expect(comparer.added).to eq([head_vulnerability])
- end
-
- it 'returns fixed vulnerability when head is empty and base is not empty' do
- comparer = described_class.new(project, base_report, empty_report)
-
- expect(comparer.fixed).to eq([base_vulnerability])
- expect(comparer.added).to eq([])
- end
- end
- end
-
- def vuln_params(project_id, identifiers, confidence: :high, severity: :critical)
- {
- project_id: project_id,
- report_type: :sast,
- identifiers: identifiers,
- confidence: ::Enums::Vulnerability.confidence_levels[confidence],
- severity: ::Enums::Vulnerability.severity_levels[severity]
- }
- end
-end
diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb
index 14f3c95ec79..9e211327dee 100644
--- a/spec/lib/gitlab/ci/runner_releases_spec.rb
+++ b/spec/lib/gitlab/ci/runner_releases_spec.rb
@@ -177,6 +177,16 @@ RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do
it 'returns parsed and sorted Gitlab::VersionInfo objects' do
expect(releases).to eq(expected_result)
end
+
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'returns nil' do
+ expect(releases).to be_nil
+ end
+ end
end
context 'when response contains unexpected input type' do
@@ -218,6 +228,16 @@ RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do
it 'returns parsed and grouped Gitlab::VersionInfo objects' do
expect(releases_by_minor).to eq(expected_result)
end
+
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'returns nil' do
+ expect(releases_by_minor).to be_nil
+ end
+ end
end
context 'when response contains unexpected input type' do
@@ -233,6 +253,18 @@ RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do
end
end
+ describe '#enabled?' do
+ it { is_expected.to be_enabled }
+
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it { is_expected.not_to be_enabled }
+ end
+ end
+
def mock_http_response(response)
http_response = instance_double(HTTParty::Response)
diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
index 07cfa939623..995922b6922 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do
describe 'AUTO_BUILD_IMAGE_VERSION' do
it 'corresponds to a published image in the registry' do
registry = "https://#{template_registry_host}"
- repository = "gitlab-org/cluster-integration/auto-build-image"
+ repository = auto_build_image_repository
reference = YAML.safe_load(template.content).dig('variables', 'AUTO_BUILD_IMAGE_VERSION')
expect(public_image_exist?(registry, repository, reference)).to be true
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
index 039a6a739dd..2b9213ea921 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
@@ -23,27 +23,33 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml', feature_category: :continuo
allow(project).to receive(:default_branch).and_return(default_branch)
end
- context 'on feature branch' do
- let(:pipeline_ref) { 'feature' }
+ context 'when SAST_DISABLED="false"' do
+ before do
+ create(:ci_variable, key: 'SAST_DISABLED', value: 'false', project: project)
+ end
+
+ context 'on feature branch' do
+ let(:pipeline_ref) { 'feature' }
- it 'creates the kics-iac-sast job' do
- expect(build_names).to contain_exactly('kics-iac-sast')
+ it 'creates the kics-iac-sast job' do
+ expect(build_names).to contain_exactly('kics-iac-sast')
+ end
end
- end
- context 'on merge request' do
- let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
- let(:merge_request) { create(:merge_request, :simple, source_project: project) }
- let(:pipeline) { service.execute(merge_request).payload }
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request).payload }
- it 'creates a pipeline with the expected jobs' do
- expect(pipeline).to be_merge_request_event
- expect(pipeline.errors.full_messages).to be_empty
- expect(build_names).to match_array(%w(kics-iac-sast))
+ it 'creates a pipeline with the expected jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(pipeline.errors.full_messages).to be_empty
+ expect(build_names).to match_array(%w(kics-iac-sast))
+ end
end
end
- context 'SAST_DISABLED is set' do
+ context 'when SAST_DISABLED="true"' do
before do
create(:ci_variable, key: 'SAST_DISABLED', value: 'true', project: project)
end
diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb
index a5365ae53b8..f8770457083 100644
--- a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipeline_composition do
let_it_be(:project) { create_default(:project, :repository, create_tag: 'test').freeze }
let_it_be(:user) { create(:user) }
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index bbd3dc54e6a..215b18ea614 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, :clean_gitlab_redis_cache, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, feature_category: :pipeline_composition do
include Ci::TemplateHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 4ee122cc607..668f1173675 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :pipeline_composition do
describe '.new' do
it 'can be initialized with an array' do
variable = { key: 'VAR', value: 'value', public: true, masked: false }
diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
index 5c9f156e054..36ada9050b2 100644
--- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
@@ -47,8 +47,8 @@ module Gitlab
end
it 'returns expanded yaml config' do
- expanded_config = YAML.safe_load(config_metadata[:merged_yaml], [Symbol])
- included_config = YAML.safe_load(included_yml, [Symbol])
+ expanded_config = YAML.safe_load(config_metadata[:merged_yaml], permitted_classes: [Symbol])
+ included_config = YAML.safe_load(included_yml, permitted_classes: [Symbol])
expect(expanded_config).to include(*included_config.keys)
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 360686ce65c..b00d9b46bc7 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
module Gitlab
module Ci
- RSpec.describe YamlProcessor, feature_category: :pipeline_authoring do
+ RSpec.describe YamlProcessor, feature_category: :pipeline_composition do
include StubRequests
include RepoHelpers
diff --git a/spec/lib/gitlab/color_schemes_spec.rb b/spec/lib/gitlab/color_schemes_spec.rb
index feb5648ff2d..bc69c8beeda 100644
--- a/spec/lib/gitlab/color_schemes_spec.rb
+++ b/spec/lib/gitlab/color_schemes_spec.rb
@@ -21,8 +21,9 @@ RSpec.describe Gitlab::ColorSchemes do
end
describe '.default' do
- it 'returns the default scheme' do
- expect(described_class.default.id).to eq 1
+ it 'use config default' do
+ stub_application_setting(default_syntax_highlighting_theme: 2)
+ expect(described_class.default.id).to eq 2
end
end
@@ -36,7 +37,8 @@ RSpec.describe Gitlab::ColorSchemes do
describe '.for_user' do
it 'returns default when user is nil' do
- expect(described_class.for_user(nil).id).to eq 1
+ stub_application_setting(default_syntax_highlighting_theme: 2)
+ expect(described_class.for_user(nil).id).to eq 2
end
it "returns user's preferred color scheme" do
diff --git a/spec/lib/gitlab/config/entry/validators_spec.rb b/spec/lib/gitlab/config/entry/validators_spec.rb
index 54a2adbefd2..abf3dbacb3d 100644
--- a/spec/lib/gitlab/config/entry/validators_spec.rb
+++ b/spec/lib/gitlab/config/entry/validators_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Config::Entry::Validators, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Config::Entry::Validators, feature_category: :pipeline_composition do
let(:klass) do
Class.new do
include ActiveModel::Validations
diff --git a/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
index bae98f9bc35..f63aacecce6 100644
--- a/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
@@ -2,26 +2,121 @@
require 'spec_helper'
-RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline_composition do
let(:loader) { described_class.new(yml, max_documents: 2) }
describe '#load!' do
- let(:yml) do
- <<~YAML
- spec:
- inputs:
- test_input:
- ---
- test_job:
- script: echo "$[[ inputs.test_input ]]"
- YAML
+ context 'when a simple single delimiter is being used' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ env:
+ ---
+ test:
+ script: echo "$[[ inputs.env ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to contain_exactly(
+ { spec: { inputs: { env: nil } } },
+ { test: { script: 'echo "$[[ inputs.env ]]"' } }
+ )
+ end
+ end
+
+ context 'when the delimiter has a trailing configuration' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ test_input:
+ --- !test/content
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to contain_exactly(
+ { spec: { inputs: { test_input: nil } } },
+ { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
+ )
+ end
+ end
+
+ context 'when the YAML file has a leading delimiter' do
+ let(:yml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ --- !test/content
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to contain_exactly(
+ { spec: { inputs: { test_input: nil } } },
+ { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
+ )
+ end
+ end
+
+ context 'when the delimiter is followed by content on the same line' do
+ let(:yml) do
+ <<~YAML
+ --- a: 1
+ --- b: 2
+ YAML
+ end
+
+ it 'loads the content as part of the document' do
+ expect(loader.load!).to contain_exactly({ a: 1 }, { b: 2 })
+ end
end
- it 'returns the loaded YAML with all keys as symbols' do
- expect(loader.load!).to eq([
- { spec: { inputs: { test_input: nil } } },
- { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
- ])
+ context 'when the delimiter does not have trailing whitespace' do
+ let(:yml) do
+ <<~YAML
+ --- a: 1
+ ---b: 2
+ YAML
+ end
+
+ it 'is not a valid delimiter' do
+ expect(loader.load!).to contain_exactly({ :'---b' => 2, a: 1 }) # rubocop:disable Style/HashSyntax
+ end
+ end
+
+ context 'when the YAML file has whitespace preceding the content' do
+ let(:yml) do
+ <<-EOYML
+ variables:
+ SUPPORTED: "parsed"
+
+ workflow:
+ rules:
+ - if: $VAR == "value"
+
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'loads everything correctly' do
+ expect(loader.load!).to contain_exactly(
+ {
+ variables: { SUPPORTED: 'parsed' },
+ workflow: { rules: [{ if: '$VAR == "value"' }] },
+ hello: { script: 'echo world' }
+ }
+ )
+ end
end
context 'when the YAML file is empty' do
@@ -32,67 +127,68 @@ RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline
end
end
- context 'when the parsed YAML is too big' do
+ context 'when there are more than the maximum number of documents' do
let(:yml) do
<<~YAML
- a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
- b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
- c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
- d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
- e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
- f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
- g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
- h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
- i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
- ---
- a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
- b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
- c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
- d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
- e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
- f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
- g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
- h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
- i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
+ --- a: 1
+ --- b: 2
+ --- c: 3
+ --- d: 4
YAML
end
- it 'raises a DataTooLargeError' do
- expect { loader.load! }.to raise_error(described_class::DataTooLargeError, 'The parsed YAML is too big')
+ it 'stops splitting documents after the maximum number' do
+ expect(loader.load!).to contain_exactly({ a: 1 }, { b: 2 })
end
end
+ end
+
+ describe '#load_raw!' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ test_input:
+ --- !test/content
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as strings' do
+ expect(loader.load_raw!).to contain_exactly(
+ { 'spec' => { 'inputs' => { 'test_input' => nil } } },
+ { 'test_job' => { 'script' => 'echo "$[[ inputs.test_input ]]"' } }
+ )
+ end
+ end
- context 'when a document is not a hash' do
+ describe '#valid?' do
+ context 'when a document is invalid' do
let(:yml) do
<<~YAML
- not_a_hash
+ a: b
---
- test_job:
- script: echo "$[[ inputs.test_input ]]"
+ c
YAML
end
- it 'raises a NotHashError' do
- expect { loader.load! }.to raise_error(described_class::NotHashError, 'Invalid configuration format')
+ it 'returns false' do
+ expect(loader).not_to be_valid
end
end
- context 'when there are too many documents' do
+ context 'when the number of documents is below the maximum and all documents are valid' do
let(:yml) do
<<~YAML
a: b
---
c: d
- ---
- e: f
YAML
end
- it 'raises a TooManyDocumentsError' do
- expect { loader.load! }.to raise_error(
- described_class::TooManyDocumentsError,
- 'The parsed YAML has too many documents'
- )
+ it 'returns true' do
+ expect(loader).to be_valid
end
end
end
diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index 346424d1681..bba66f33718 100644
--- a/spec/lib/gitlab/config/loader/yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_authoring do
+RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_composition do
let(:loader) { described_class.new(yml) }
let(:yml) do
@@ -182,4 +182,30 @@ RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_authori
)
end
end
+
+ describe '#blank?' do
+ context 'when the loaded YAML is empty' do
+ let(:yml) do
+ <<~YAML
+ # only comments here
+ YAML
+ end
+
+ it 'returns true' do
+ expect(loader).to be_blank
+ end
+ end
+
+ context 'when the loaded YAML has content' do
+ let(:yml) do
+ <<~YAML
+ test: value
+ YAML
+ end
+
+ it 'returns false' do
+ expect(loader).not_to be_blank
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/console_spec.rb b/spec/lib/gitlab/console_spec.rb
index f043433b4c5..5723a4421f6 100644
--- a/spec/lib/gitlab/console_spec.rb
+++ b/spec/lib/gitlab/console_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::Console do
+RSpec.describe Gitlab::Console, feature_category: :application_instrumentation do
describe '.welcome!' do
context 'when running in the Rails console' do
before do
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index f298890623f..ffb651fe23c 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -102,11 +102,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
describe 'Zuora directives' do
- context 'when is Gitlab.com?' do
- before do
- allow(::Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'when on SaaS', :saas do
it 'adds Zuora host to CSP' do
expect(directives['frame_src']).to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
end
@@ -182,6 +178,53 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
end
end
+ context 'when KAS is configured' do
+ before do
+ stub_config_setting(host: 'gitlab.example.com')
+ allow(::Gitlab::Kas).to receive(:enabled?).and_return true
+ end
+
+ context 'when user access feature flag is disabled' do
+ before do
+ stub_feature_flags(kas_user_access: false)
+ end
+
+ it 'does not add KAS url to CSP' do
+ expect(directives['connect_src']).not_to eq("'self' ws://gitlab.example.com #{::Gitlab::Kas.tunnel_url}")
+ end
+ end
+
+ context 'when user access feature flag is enabled' do
+ before do
+ stub_feature_flags(kas_user_access: true)
+ end
+
+ context 'when KAS is on same domain as rails' do
+ let_it_be(:kas_tunnel_url) { "ws://gitlab.example.com/-/k8s-proxy/" }
+
+ before do
+ allow(::Gitlab::Kas).to receive(:tunnel_url).and_return(kas_tunnel_url)
+ end
+
+ it 'does not add KAS url to CSP' do
+ expect(directives['connect_src']).not_to eq("'self' ws://gitlab.example.com #{::Gitlab::Kas.tunnel_url}")
+ end
+ end
+
+ context 'when KAS is on subdomain' do
+ let_it_be(:kas_tunnel_url) { "ws://kas.gitlab.example.com/k8s-proxy/" }
+
+ before do
+ allow(::Gitlab::Kas).to receive(:tunnel_url).and_return(kas_tunnel_url)
+ end
+
+ it 'does add KAS url to CSP' do
+ expect(directives['connect_src']).to eq("'self' ws://gitlab.example.com #{kas_tunnel_url}")
+ end
+ end
+ end
+ end
+
context 'when CUSTOMER_PORTAL_URL is set' do
let(:customer_portal_url) { 'https://customers.example.com' }
diff --git a/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb
new file mode 100644
index 00000000000..4dd510499ab
--- /dev/null
+++ b/spec/lib/gitlab/database/async_constraints/migration_helpers_spec.rb
@@ -0,0 +1,288 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncConstraints::MigrationHelpers, feature_category: :database do
+ let(:migration) { Gitlab::Database::Migration[2.1].new }
+ let(:connection) { ApplicationRecord.connection }
+ let(:constraint_model) { Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation }
+ let(:table_name) { '_test_async_fks' }
+ let(:column_name) { 'parent_id' }
+ let(:fk_name) { nil }
+
+ context 'with async FK validation on regular tables' do
+ before do
+ allow(migration).to receive(:puts)
+ allow(migration.connection).to receive(:transaction_open?).and_return(false)
+
+ connection.create_table(table_name) do |t|
+ t.integer column_name
+ end
+
+ migration.add_concurrent_foreign_key(
+ table_name, table_name,
+ column: column_name, validate: false, name: fk_name)
+ end
+
+ describe '#prepare_async_foreign_key_validation' do
+ it 'creates the record for the async FK validation' do
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, column_name)
+ end.to change { constraint_model.where(table_name: table_name).count }.by(1)
+
+ record = constraint_model.find_by(table_name: table_name)
+
+ expect(record.name).to start_with('fk_')
+ expect(record).to be_foreign_key
+ end
+
+ context 'when an explicit name is given' do
+ let(:fk_name) { 'my_fk_name' }
+
+ it 'creates the record with the given name' do
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
+ end.to change { constraint_model.where(name: fk_name).count }.by(1)
+
+ record = constraint_model.find_by(name: fk_name)
+
+ expect(record.table_name).to eq(table_name)
+ expect(record).to be_foreign_key
+ end
+ end
+
+ context 'when the FK does not exist' do
+ it 'returns an error' do
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, name: 'no_fk')
+ end.to raise_error RuntimeError, /Could not find foreign key "no_fk" on table "_test_async_fks"/
+ end
+ end
+
+ context 'when the record already exists' do
+ let(:fk_name) { 'my_fk_name' }
+
+ it 'does attempt to create the record' do
+ create(:postgres_async_constraint_validation, table_name: table_name, name: fk_name)
+
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
+ end.not_to change { constraint_model.where(name: fk_name).count }
+ end
+ end
+
+ context 'when the async FK validation table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(constraint_model.table_name)
+
+ expect(constraint_model).not_to receive(:safe_find_or_create_by!)
+
+ expect { migration.prepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#unprepare_async_foreign_key_validation' do
+ context 'with foreign keys' do
+ before do
+ migration.prepare_async_foreign_key_validation(table_name, column_name, name: fk_name)
+ end
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_foreign_key_validation(table_name, column_name)
+ end.to change { constraint_model.where(table_name: table_name).count }.by(-1)
+ end
+
+ context 'when an explicit name is given' do
+ let(:fk_name) { 'my_test_async_fk' }
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_foreign_key_validation(table_name, name: fk_name)
+ end.to change { constraint_model.where(name: fk_name).count }.by(-1)
+ end
+ end
+
+ context 'when the async fk validation table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(constraint_model.table_name)
+
+ expect(constraint_model).not_to receive(:find_by)
+
+ expect { migration.unprepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
+ end
+ end
+ end
+
+ context 'with other types of constraints' do
+ let(:name) { 'my_test_async_constraint' }
+ let(:constraint) { create(:postgres_async_constraint_validation, table_name: table_name, name: name) }
+
+ it 'does not destroy the record' do
+ constraint.update_column(:constraint_type, 99)
+
+ expect do
+ migration.unprepare_async_foreign_key_validation(table_name, name: name)
+ end.not_to change { constraint_model.where(name: name).count }
+
+ expect(constraint).to be_present
+ end
+ end
+ end
+ end
+
+ context 'with partitioned tables' do
+ let(:partition_schema) { 'gitlab_partitions_dynamic' }
+ let(:partition1_name) { "#{partition_schema}.#{table_name}_202001" }
+ let(:partition2_name) { "#{partition_schema}.#{table_name}_202002" }
+ let(:fk_name) { 'my_partitioned_fk_name' }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id serial NOT NULL,
+ #{column_name} int NOT NULL,
+ created_at timestamptz NOT NULL,
+ PRIMARY KEY (id, created_at)
+ ) PARTITION BY RANGE (created_at);
+
+ CREATE TABLE #{partition1_name} PARTITION OF #{table_name}
+ FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
+
+ CREATE TABLE #{partition2_name} PARTITION OF #{table_name}
+ FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
+ SQL
+ end
+
+ describe '#prepare_partitioned_async_foreign_key_validation' do
+ it 'delegates to prepare_async_foreign_key_validation for each partition' do
+ expect(migration)
+ .to receive(:prepare_async_foreign_key_validation)
+ .with(partition1_name, column_name, name: fk_name)
+
+ expect(migration)
+ .to receive(:prepare_async_foreign_key_validation)
+ .with(partition2_name, column_name, name: fk_name)
+
+ migration.prepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
+ end
+ end
+
+ describe '#unprepare_partitioned_async_foreign_key_validation' do
+ it 'delegates to unprepare_async_foreign_key_validation for each partition' do
+ expect(migration)
+ .to receive(:unprepare_async_foreign_key_validation)
+ .with(partition1_name, column_name, name: fk_name)
+
+ expect(migration)
+ .to receive(:unprepare_async_foreign_key_validation)
+ .with(partition2_name, column_name, name: fk_name)
+
+ migration.unprepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
+ end
+ end
+ end
+
+ context 'with async check constraint validations' do
+ let(:table_name) { '_test_async_check_constraints' }
+ let(:check_name) { 'partitioning_constraint' }
+
+ before do
+ allow(migration).to receive(:puts)
+ allow(migration.connection).to receive(:transaction_open?).and_return(false)
+
+ connection.create_table(table_name) do |t|
+ t.integer column_name
+ end
+
+ migration.add_check_constraint(
+ table_name, "#{column_name} = 1",
+ check_name, validate: false)
+ end
+
+ describe '#prepare_async_check_constraint_validation' do
+ it 'creates the record for async validation' do
+ expect do
+ migration.prepare_async_check_constraint_validation(table_name, name: check_name)
+ end.to change { constraint_model.where(name: check_name).count }.by(1)
+
+ record = constraint_model.find_by(name: check_name)
+
+ expect(record.table_name).to eq(table_name)
+ expect(record).to be_check_constraint
+ end
+
+ context 'when the check constraint does not exist' do
+ it 'returns an error' do
+ expect do
+ migration.prepare_async_check_constraint_validation(table_name, name: 'missing')
+ end.to raise_error RuntimeError, /Could not find check constraint "missing" on table "#{table_name}"/
+ end
+ end
+
+ context 'when the record already exists' do
+ it 'does attempt to create the record' do
+ create(:postgres_async_constraint_validation,
+ table_name: table_name,
+ name: check_name,
+ constraint_type: :check_constraint)
+
+ expect do
+ migration.prepare_async_check_constraint_validation(table_name, name: check_name)
+ end.not_to change { constraint_model.where(name: check_name).count }
+ end
+ end
+
+ context 'when the async validation table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(constraint_model.table_name)
+
+ expect(constraint_model).not_to receive(:safe_find_or_create_by!)
+
+ expect { migration.prepare_async_check_constraint_validation(table_name, name: check_name) }
+ .not_to raise_error
+ end
+ end
+ end
+
+ describe '#unprepare_async_check_constraint_validation' do
+ context 'with check constraints' do
+ before do
+ migration.prepare_async_check_constraint_validation(table_name, name: check_name)
+ end
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_check_constraint_validation(table_name, name: check_name)
+ end.to change { constraint_model.where(name: check_name).count }.by(-1)
+ end
+
+ context 'when the async validation table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(constraint_model.table_name)
+
+ expect(constraint_model).not_to receive(:find_by)
+
+ expect { migration.unprepare_async_check_constraint_validation(table_name, name: check_name) }
+ .not_to raise_error
+ end
+ end
+ end
+
+ context 'with other types of constraints' do
+ let(:constraint) { create(:postgres_async_constraint_validation, table_name: table_name, name: check_name) }
+
+ it 'does not destroy the record' do
+ constraint.update_column(:constraint_type, 99)
+
+ expect do
+ migration.unprepare_async_check_constraint_validation(table_name, name: check_name)
+ end.not_to change { constraint_model.where(name: check_name).count }
+
+ expect(constraint).to be_present
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb
new file mode 100644
index 00000000000..52fbf6d2f9b
--- /dev/null
+++ b/spec/lib/gitlab/database/async_constraints/postgres_async_constraint_validation_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation, type: :model,
+ feature_category: :database do
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
+ describe 'validations' do
+ let_it_be(:constraint_validation) { create(:postgres_async_constraint_validation) }
+ let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH }
+ let(:last_error_limit) { described_class::MAX_LAST_ERROR_LENGTH }
+
+ subject { constraint_validation }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:table_name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) }
+ it { is_expected.to validate_presence_of(:table_name) }
+ it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) }
+ it { is_expected.to validate_length_of(:last_error).is_at_most(last_error_limit) }
+ end
+
+ describe 'scopes' do
+ let!(:failed_validation) { create(:postgres_async_constraint_validation, attempts: 1) }
+ let!(:new_validation) { create(:postgres_async_constraint_validation) }
+
+ describe '.ordered' do
+ subject { described_class.ordered }
+
+ it { is_expected.to eq([new_validation, failed_validation]) }
+ end
+
+ describe '.foreign_key_type' do
+ before do
+ new_validation.update_column(:constraint_type, 99)
+ end
+
+ subject { described_class.foreign_key_type }
+
+ it { is_expected.to eq([failed_validation]) }
+
+ it 'does not apply the filter if the column is not present' do
+ expect(described_class)
+ .to receive(:constraint_type_exists?)
+ .and_return(false)
+
+ is_expected.to match_array([failed_validation, new_validation])
+ end
+ end
+
+ describe '.check_constraint_type' do
+ before do
+ new_validation.update!(constraint_type: :check_constraint)
+ end
+
+ subject { described_class.check_constraint_type }
+
+ it { is_expected.to eq([new_validation]) }
+ end
+ end
+
+ describe '.table_available?' do
+ subject { described_class.table_available? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when the table does not exist' do
+ before do
+ described_class
+ .connection
+ .drop_table(described_class.table_name)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '.constraint_type_exists?' do
+ it { expect(described_class.constraint_type_exists?).to be_truthy }
+
+ it 'always asks the database' do
+ control = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) do
+ described_class.constraint_type_exists?
+ end
+
+ expect(control.count).to be >= 1
+ expect { described_class.constraint_type_exists? }.to issue_same_number_of_queries_as(control)
+ end
+ end
+
+ describe '#handle_exception!' do
+ let_it_be_with_reload(:constraint_validation) { create(:postgres_async_constraint_validation) }
+
+ let(:error) { instance_double(StandardError, message: 'Oups', backtrace: %w[this that]) }
+
+ subject { constraint_validation.handle_exception!(error) }
+
+ it 'increases the attempts number' do
+ expect { subject }.to change { constraint_validation.reload.attempts }.by(1)
+ end
+
+ it 'saves error details' do
+ subject
+
+ expect(constraint_validation.reload.last_error).to eq("Oups\nthis\nthat")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_constraints/validators/check_constraint_spec.rb b/spec/lib/gitlab/database/async_constraints/validators/check_constraint_spec.rb
new file mode 100644
index 00000000000..7622b39feb1
--- /dev/null
+++ b/spec/lib/gitlab/database/async_constraints/validators/check_constraint_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncConstraints::Validators::CheckConstraint, feature_category: :database do
+ it_behaves_like 'async constraints validation' do
+ let(:constraint_type) { :check_constraint }
+
+ before do
+ connection.create_table(table_name) do |t|
+ t.integer :parent_id
+ end
+
+ connection.execute(<<~SQL.squish)
+ ALTER TABLE #{table_name} ADD CONSTRAINT #{constraint_name}
+ CHECK ( parent_id = 101 ) NOT VALID;
+ SQL
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_constraints/validators/foreign_key_spec.rb b/spec/lib/gitlab/database/async_constraints/validators/foreign_key_spec.rb
new file mode 100644
index 00000000000..0e345e0e9ae
--- /dev/null
+++ b/spec/lib/gitlab/database/async_constraints/validators/foreign_key_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncConstraints::Validators::ForeignKey, feature_category: :database do
+ it_behaves_like 'async constraints validation' do
+ let(:constraint_type) { :foreign_key }
+
+ before do
+ connection.create_table(table_name) do |t|
+ t.references :parent, foreign_key: { to_table: table_name, validate: false, name: constraint_name }
+ end
+ end
+
+ context 'with fully qualified table names' do
+ let(:validation) do
+ create(:postgres_async_constraint_validation,
+ table_name: "public.#{table_name}",
+ name: constraint_name,
+ constraint_type: constraint_type
+ )
+ end
+
+ it 'validates the constraint' do
+ allow(connection).to receive(:execute).and_call_original
+
+ expect(connection).to receive(:execute)
+ .with(/ALTER TABLE "public"."#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/)
+ .ordered.and_call_original
+
+ subject.perform
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_constraints/validators_spec.rb b/spec/lib/gitlab/database/async_constraints/validators_spec.rb
new file mode 100644
index 00000000000..e903b79dd1b
--- /dev/null
+++ b/spec/lib/gitlab/database/async_constraints/validators_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncConstraints::Validators, feature_category: :database do
+ describe '.for' do
+ subject { described_class.for(record) }
+
+ context 'with foreign keys validations' do
+ let(:record) { build(:postgres_async_constraint_validation, :foreign_key) }
+
+ it { is_expected.to be_a(described_class::ForeignKey) }
+ end
+
+ context 'with check constraint validations' do
+ let(:record) { build(:postgres_async_constraint_validation, :check_constraint) }
+
+ it { is_expected.to be_a(described_class::CheckConstraint) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_constraints_spec.rb b/spec/lib/gitlab/database/async_constraints_spec.rb
new file mode 100644
index 00000000000..e5cf782485f
--- /dev/null
+++ b/spec/lib/gitlab/database/async_constraints_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncConstraints, feature_category: :database do
+ describe '.validate_pending_entries!' do
+ subject { described_class.validate_pending_entries! }
+
+ let!(:fk_validation) do
+ create(:postgres_async_constraint_validation, :foreign_key, attempts: 2)
+ end
+
+ let(:check_validation) do
+ create(:postgres_async_constraint_validation, :check_constraint, attempts: 1)
+ end
+
+ it 'executes pending validations' do
+ expect_next_instance_of(described_class::Validators::ForeignKey, fk_validation) do |validator|
+ expect(validator).to receive(:perform)
+ end
+
+ expect_next_instance_of(described_class::Validators::CheckConstraint, check_validation) do |validator|
+ expect(validator).to receive(:perform)
+ end
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb
deleted file mode 100644
index 90137e259f5..00000000000
--- a/spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::AsyncForeignKeys::ForeignKeyValidator, feature_category: :database do
- include ExclusiveLeaseHelpers
-
- describe '#perform' do
- let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) }
- let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
- let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION }
-
- let(:fk_model) { Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation }
- let(:table_name) { '_test_async_fks' }
- let(:fk_name) { 'fk_parent_id' }
- let(:validation) { create(:postgres_async_foreign_key_validation, table_name: table_name, name: fk_name) }
- let(:connection) { validation.connection }
-
- subject { described_class.new(validation) }
-
- before do
- connection.create_table(table_name) do |t|
- t.references :parent, foreign_key: { to_table: table_name, validate: false, name: fk_name }
- end
- end
-
- it 'validates the FK while controlling statement timeout' do
- allow(connection).to receive(:execute).and_call_original
- expect(connection).to receive(:execute)
- .with("SET statement_timeout TO '43200s'").ordered.and_call_original
- expect(connection).to receive(:execute)
- .with('ALTER TABLE "_test_async_fks" VALIDATE CONSTRAINT "fk_parent_id";').ordered.and_call_original
- expect(connection).to receive(:execute)
- .with("RESET statement_timeout").ordered.and_call_original
-
- subject.perform
- end
-
- context 'with fully qualified table names' do
- let(:validation) do
- create(:postgres_async_foreign_key_validation,
- table_name: "public.#{table_name}",
- name: fk_name
- )
- end
-
- it 'validates the FK' do
- allow(connection).to receive(:execute).and_call_original
-
- expect(connection).to receive(:execute)
- .with('ALTER TABLE "public"."_test_async_fks" VALIDATE CONSTRAINT "fk_parent_id";').ordered.and_call_original
-
- subject.perform
- end
- end
-
- it 'removes the FK validation record from table' do
- expect(validation).to receive(:destroy!).and_call_original
-
- expect { subject.perform }.to change { fk_model.count }.by(-1)
- end
-
- it 'skips logic if not able to acquire exclusive lease' do
- expect(lease).to receive(:try_obtain).ordered.and_return(false)
- expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
- expect(validation).not_to receive(:destroy!)
-
- expect { subject.perform }.not_to change { fk_model.count }
- end
-
- it 'logs messages around execution' do
- allow(Gitlab::AppLogger).to receive(:info).and_call_original
-
- subject.perform
-
- expect(Gitlab::AppLogger)
- .to have_received(:info)
- .with(a_hash_including(message: 'Starting to validate foreign key'))
-
- expect(Gitlab::AppLogger)
- .to have_received(:info)
- .with(a_hash_including(message: 'Finished validating foreign key'))
- end
-
- context 'when the FK does not exist' do
- before do
- connection.create_table(table_name, force: true)
- end
-
- it 'skips validation and removes the record' do
- expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
-
- expect { subject.perform }.to change { fk_model.count }.by(-1)
- end
-
- it 'logs an appropriate message' do
- expected_message = "Skipping #{fk_name} validation since it does not exist. The queuing entry will be deleted"
-
- allow(Gitlab::AppLogger).to receive(:info).and_call_original
-
- subject.perform
-
- expect(Gitlab::AppLogger)
- .to have_received(:info)
- .with(a_hash_including(message: expected_message))
- end
- end
-
- context 'with error handling' do
- before do
- allow(connection).to receive(:execute).and_call_original
-
- allow(connection).to receive(:execute)
- .with('ALTER TABLE "_test_async_fks" VALIDATE CONSTRAINT "fk_parent_id";')
- .and_raise(ActiveRecord::StatementInvalid)
- end
-
- context 'on production' do
- before do
- allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
- end
-
- it 'increases execution attempts' do
- expect { subject.perform }.to change { validation.attempts }.by(1)
-
- expect(validation.last_error).to be_present
- expect(validation).not_to be_destroyed
- end
-
- it 'logs an error message including the fk_name' do
- expect(Gitlab::AppLogger)
- .to receive(:error)
- .with(a_hash_including(:message, :fk_name))
- .and_call_original
-
- subject.perform
- end
- end
-
- context 'on development' do
- it 'also raises errors' do
- expect { subject.perform }
- .to raise_error(ActiveRecord::StatementInvalid)
- .and change { validation.attempts }.by(1)
-
- expect(validation.last_error).to be_present
- expect(validation).not_to be_destroyed
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb
deleted file mode 100644
index 0bd0e8045ff..00000000000
--- a/spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::AsyncForeignKeys::MigrationHelpers, feature_category: :database do
- let(:migration) { Gitlab::Database::Migration[2.1].new }
- let(:connection) { ApplicationRecord.connection }
- let(:fk_model) { Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation }
- let(:table_name) { '_test_async_fks' }
- let(:column_name) { 'parent_id' }
- let(:fk_name) { nil }
-
- context 'with regular tables' do
- before do
- allow(migration).to receive(:puts)
- allow(migration.connection).to receive(:transaction_open?).and_return(false)
-
- connection.create_table(table_name) do |t|
- t.integer column_name
- end
-
- migration.add_concurrent_foreign_key(
- table_name, table_name,
- column: column_name, validate: false, name: fk_name)
- end
-
- describe '#prepare_async_foreign_key_validation' do
- it 'creates the record for the async FK validation' do
- expect do
- migration.prepare_async_foreign_key_validation(table_name, column_name)
- end.to change { fk_model.where(table_name: table_name).count }.by(1)
-
- record = fk_model.find_by(table_name: table_name)
-
- expect(record.name).to start_with('fk_')
- end
-
- context 'when an explicit name is given' do
- let(:fk_name) { 'my_fk_name' }
-
- it 'creates the record with the given name' do
- expect do
- migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
- end.to change { fk_model.where(name: fk_name).count }.by(1)
-
- record = fk_model.find_by(name: fk_name)
-
- expect(record.table_name).to eq(table_name)
- end
- end
-
- context 'when the FK does not exist' do
- it 'returns an error' do
- expect do
- migration.prepare_async_foreign_key_validation(table_name, name: 'no_fk')
- end.to raise_error RuntimeError, /Could not find foreign key "no_fk" on table "_test_async_fks"/
- end
- end
-
- context 'when the record already exists' do
- let(:fk_name) { 'my_fk_name' }
-
- it 'does attempt to create the record' do
- create(:postgres_async_foreign_key_validation, table_name: table_name, name: fk_name)
-
- expect do
- migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
- end.not_to change { fk_model.where(name: fk_name).count }
- end
- end
-
- context 'when the async FK validation table does not exist' do
- it 'does not raise an error' do
- connection.drop_table(:postgres_async_foreign_key_validations)
-
- expect(fk_model).not_to receive(:safe_find_or_create_by!)
-
- expect { migration.prepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
- end
- end
- end
-
- describe '#unprepare_async_foreign_key_validation' do
- before do
- migration.prepare_async_foreign_key_validation(table_name, column_name, name: fk_name)
- end
-
- it 'destroys the record' do
- expect do
- migration.unprepare_async_foreign_key_validation(table_name, column_name)
- end.to change { fk_model.where(table_name: table_name).count }.by(-1)
- end
-
- context 'when an explicit name is given' do
- let(:fk_name) { 'my_test_async_fk' }
-
- it 'destroys the record' do
- expect do
- migration.unprepare_async_foreign_key_validation(table_name, name: fk_name)
- end.to change { fk_model.where(name: fk_name).count }.by(-1)
- end
- end
-
- context 'when the async fk validation table does not exist' do
- it 'does not raise an error' do
- connection.drop_table(:postgres_async_foreign_key_validations)
-
- expect(fk_model).not_to receive(:find_by)
-
- expect { migration.unprepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
- end
- end
- end
- end
-
- context 'with partitioned tables' do
- let(:partition_schema) { 'gitlab_partitions_dynamic' }
- let(:partition1_name) { "#{partition_schema}.#{table_name}_202001" }
- let(:partition2_name) { "#{partition_schema}.#{table_name}_202002" }
- let(:fk_name) { 'my_partitioned_fk_name' }
-
- before do
- connection.execute(<<~SQL)
- CREATE TABLE #{table_name} (
- id serial NOT NULL,
- #{column_name} int NOT NULL,
- created_at timestamptz NOT NULL,
- PRIMARY KEY (id, created_at)
- ) PARTITION BY RANGE (created_at);
-
- CREATE TABLE #{partition1_name} PARTITION OF #{table_name}
- FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
-
- CREATE TABLE #{partition2_name} PARTITION OF #{table_name}
- FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
- SQL
- end
-
- describe '#prepare_partitioned_async_foreign_key_validation' do
- it 'delegates to prepare_async_foreign_key_validation for each partition' do
- expect(migration)
- .to receive(:prepare_async_foreign_key_validation)
- .with(partition1_name, column_name, name: fk_name)
-
- expect(migration)
- .to receive(:prepare_async_foreign_key_validation)
- .with(partition2_name, column_name, name: fk_name)
-
- migration.prepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
- end
- end
-
- describe '#unprepare_partitioned_async_foreign_key_validation' do
- it 'delegates to unprepare_async_foreign_key_validation for each partition' do
- expect(migration)
- .to receive(:unprepare_async_foreign_key_validation)
- .with(partition1_name, column_name, name: fk_name)
-
- expect(migration)
- .to receive(:unprepare_async_foreign_key_validation)
- .with(partition2_name, column_name, name: fk_name)
-
- migration.unprepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb
deleted file mode 100644
index ba201d93f52..00000000000
--- a/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation, type: :model,
- feature_category: :database do
- it { is_expected.to be_a Gitlab::Database::SharedModel }
-
- describe 'validations' do
- let_it_be(:fk_validation) { create(:postgres_async_foreign_key_validation) }
- let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH }
- let(:last_error_limit) { described_class::MAX_LAST_ERROR_LENGTH }
-
- subject { fk_validation }
-
- it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_uniqueness_of(:name) }
- it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) }
- it { is_expected.to validate_presence_of(:table_name) }
- it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) }
- it { is_expected.to validate_length_of(:last_error).is_at_most(last_error_limit) }
- end
-
- describe 'scopes' do
- let!(:failed_validation) { create(:postgres_async_foreign_key_validation, attempts: 1) }
- let!(:new_validation) { create(:postgres_async_foreign_key_validation) }
-
- describe '.ordered' do
- subject { described_class.ordered }
-
- it { is_expected.to eq([new_validation, failed_validation]) }
- end
- end
-
- describe '#handle_exception!' do
- let_it_be_with_reload(:fk_validation) { create(:postgres_async_foreign_key_validation) }
-
- let(:error) { instance_double(StandardError, message: 'Oups', backtrace: %w[this that]) }
-
- subject { fk_validation.handle_exception!(error) }
-
- it 'increases the attempts number' do
- expect { subject }.to change { fk_validation.reload.attempts }.by(1)
- end
-
- it 'saves error details' do
- subject
-
- expect(fk_validation.reload.last_error).to eq("Oups\nthis\nthat")
- end
- end
-end
diff --git a/spec/lib/gitlab/database/async_foreign_keys_spec.rb b/spec/lib/gitlab/database/async_foreign_keys_spec.rb
deleted file mode 100644
index f15eb364929..00000000000
--- a/spec/lib/gitlab/database/async_foreign_keys_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::AsyncForeignKeys, feature_category: :database do
- describe '.validate_pending_entries!' do
- subject { described_class.validate_pending_entries! }
-
- before do
- create_list(:postgres_async_foreign_key_validation, 3)
- end
-
- it 'takes 2 pending FK validations and executes them' do
- validations = described_class::PostgresAsyncForeignKeyValidation.ordered.limit(2).to_a
-
- expect_next_instances_of(described_class::ForeignKeyValidator, 2, validations) do |validator|
- expect(validator).to receive(:perform)
- end
-
- subject
- end
- end
-end
diff --git a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
index c367f4a4493..fb9b16d46d6 100644
--- a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
@@ -113,5 +113,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do
expect { subject }.to change { migration.reload.batch_size }.to(1_000)
end
end
+
+ context 'when migration max_batch_size is less than MIN_BATCH_SIZE' do
+ let(:migration_params) { { max_batch_size: 900 } }
+
+ it 'does not raise an error' do
+ mock_efficiency(0.7)
+ expect { subject }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index cc9f3d5b7f1..073a30e7839 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -184,6 +184,35 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
expect(transition_log.exception_message).to eq('RuntimeError')
end
end
+
+ context 'when job fails during sub batch processing' do
+ let(:args) { { error: ActiveRecord::StatementTimeout.new, from_sub_batch: true } }
+ let(:attempts) { 0 }
+ let(:failure) { job.failure!(**args) }
+ let(:job) do
+ create(:batched_background_migration_job, :running, batch_size: 20, sub_batch_size: 10, attempts: attempts)
+ end
+
+ context 'when sub batch size can be reduced in 25%' do
+ it { expect { failure }.to change { job.sub_batch_size }.to 7 }
+ end
+
+ context 'when retries exceeds 2 attempts' do
+ let(:attempts) { 3 }
+
+ before do
+ allow(job).to receive(:split_and_retry!)
+ end
+
+ it 'calls split_and_retry! once sub_batch_size cannot be decreased anymore' do
+ failure
+
+ expect(job).to have_received(:split_and_retry!).once
+ end
+
+ it { expect { failure }.not_to change { job.sub_batch_size } }
+ end
+ end
end
describe 'scopes' do
@@ -271,6 +300,24 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
+ describe '.extract_transition_options' do
+ let(:perform) { subject.class.extract_transition_options(args) }
+
+ where(:args, :expected_result) do
+ [
+ [[], []],
+ [[{ error: StandardError }], [StandardError, nil]],
+ [[{ error: StandardError, from_sub_batch: true }], [StandardError, true]]
+ ]
+ end
+
+ with_them do
+ it 'matches expected keys and result' do
+ expect(perform).to match_array(expected_result)
+ end
+ end
+ end
+
describe '#can_split?' do
subject { job.can_split?(exception) }
@@ -327,6 +374,48 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
+ describe '#can_reduce_sub_batch_size?' do
+ let(:attempts) { 0 }
+ let(:batch_size) { 10 }
+ let(:sub_batch_size) { 6 }
+ let(:feature_flag) { :reduce_sub_batch_size_on_timeouts }
+ let(:job) do
+ create(:batched_background_migration_job, attempts: attempts,
+ batch_size: batch_size, sub_batch_size: sub_batch_size)
+ end
+
+ where(:feature_flag_state, :within_boundaries, :outside_boundaries, :limit_reached) do
+ [
+ [true, true, false, false],
+ [false, false, false, false]
+ ]
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(feature_flag => feature_flag_state)
+ end
+
+ context 'when the number of attempts is lower than the limit and batch size are within boundaries' do
+ let(:attempts) { 1 }
+
+ it { expect(job.can_reduce_sub_batch_size?).to be(within_boundaries) }
+ end
+
+ context 'when the number of attempts is lower than the limit and batch size are outside boundaries' do
+ let(:batch_size) { 1 }
+
+ it { expect(job.can_reduce_sub_batch_size?).to be(outside_boundaries) }
+ end
+
+ context 'when the number of attempts is greater than the limit and batch size are within boundaries' do
+ let(:attempts) { 3 }
+
+ it { expect(job.can_reduce_sub_batch_size?).to be(limit_reached) }
+ end
+ end
+ end
+
describe '#time_efficiency' do
subject { job.time_efficiency }
@@ -465,4 +554,80 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
end
+
+ describe '#reduce_sub_batch_size!' do
+ let(:migration_batch_size) { 20 }
+ let(:migration_sub_batch_size) { 10 }
+ let(:job_batch_size) { 20 }
+ let(:job_sub_batch_size) { 10 }
+ let(:status) { :failed }
+
+ let(:migration) do
+ create(:batched_background_migration, :active, batch_size: migration_batch_size,
+ sub_batch_size: migration_sub_batch_size)
+ end
+
+ let(:job) do
+ create(:batched_background_migration_job, status, sub_batch_size: job_sub_batch_size,
+ batch_size: job_batch_size, batched_migration: migration)
+ end
+
+ context 'when the job sub batch size can be reduced' do
+ let(:expected_sub_batch_size) { 7 }
+
+ it 'reduces sub batch size in 25%' do
+ expect { job.reduce_sub_batch_size! }.to change { job.sub_batch_size }.to(expected_sub_batch_size)
+ end
+
+ it 'log the changes' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: 'Sub batch size reduced due to timeout',
+ batched_job_id: job.id,
+ sub_batch_size: job_sub_batch_size,
+ reduced_sub_batch_size: expected_sub_batch_size,
+ attempts: job.attempts,
+ batched_migration_id: migration.id,
+ job_class_name: job.migration_job_class_name,
+ job_arguments: job.migration_job_arguments
+ )
+
+ job.reduce_sub_batch_size!
+ end
+ end
+
+ context 'when reduced sub_batch_size is greater than sub_batch' do
+ let(:job_batch_size) { 5 }
+
+ it "doesn't allow sub_batch_size to greater than sub_batch" do
+ expect { job.reduce_sub_batch_size! }.to change { job.sub_batch_size }.to 5
+ end
+ end
+
+ context 'when sub_batch_size is already 1' do
+ let(:job_sub_batch_size) { 1 }
+
+ it "updates sub_batch_size to it's minimum value" do
+ expect { job.reduce_sub_batch_size! }.not_to change { job.sub_batch_size }
+ end
+ end
+
+ context 'when job has not failed' do
+ let(:status) { :succeeded }
+ let(:error) { Gitlab::Database::BackgroundMigration::ReduceSubBatchSizeError }
+
+ it 'raises an exception' do
+ expect { job.reduce_sub_batch_size! }.to raise_error(error)
+ end
+ end
+
+ context 'when the amount to be reduced exceeds the threshold' do
+ let(:migration_batch_size) { 150 }
+ let(:migration_sub_batch_size) { 100 }
+ let(:job_sub_batch_size) { 30 }
+
+ it 'prevents sub batch size to be reduced' do
+ expect { job.reduce_sub_batch_size! }.not_to change { job.sub_batch_size }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index f3a292abbae..8d74d16f4e5 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
let(:connection) { Gitlab::Database.database_base_models[:main].connection }
let(:metrics_tracker) { instance_double('::Gitlab::Database::BackgroundMigration::PrometheusMetrics', track: nil) }
let(:job_class) { Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) }
+ let(:sub_batch_exception) { Gitlab::Database::BackgroundMigration::SubBatchTimeoutError }
let_it_be(:pause_ms) { 250 }
let_it_be(:active_migration) { create(:batched_background_migration, :active, job_arguments: [:id, :other_id]) }
@@ -39,7 +40,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
sub_batch_size: 1,
pause_ms: pause_ms,
job_arguments: active_migration.job_arguments,
- connection: connection)
+ connection: connection,
+ sub_batch_exception: sub_batch_exception)
.and_return(job_instance)
expect(job_instance).to receive(:perform).with(no_args)
@@ -119,12 +121,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
end
context 'when the migration job raises an error' do
- shared_examples 'an error is raised' do |error_class|
+ shared_examples 'an error is raised' do |error_class, cause|
+ let(:expected_to_raise) { cause || error_class }
+
it 'marks the tracking record as failed' do
expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class)
freeze_time do
- expect { perform }.to raise_error(error_class)
+ expect { perform }.to raise_error(expected_to_raise)
reloaded_job_record = job_record.reload
@@ -137,13 +141,16 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
expect(job_instance).to receive(:perform).with(no_args).and_raise(error_class)
expect(metrics_tracker).to receive(:track).with(job_record)
- expect { perform }.to raise_error(error_class)
+ expect { perform }.to raise_error(expected_to_raise)
end
end
it_behaves_like 'an error is raised', RuntimeError.new('Something broke!')
it_behaves_like 'an error is raised', SignalException.new('SIGTERM')
it_behaves_like 'an error is raised', ActiveRecord::StatementTimeout.new('Timeout!')
+
+ error = StandardError.new
+ it_behaves_like('an error is raised', Gitlab::Database::BackgroundMigration::SubBatchTimeoutError.new(error), error)
end
context 'when the batched background migration does not inherit from BatchedMigrationJob' do
diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb
index 28a087d5401..b187b29c270 100644
--- a/spec/lib/gitlab/database/gitlab_schema_spec.rb
+++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb
@@ -16,19 +16,21 @@ RSpec.shared_examples 'validate schema data' do |tables_and_views|
end
end
-RSpec.describe Gitlab::Database::GitlabSchema do
+RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do
shared_examples 'maps table name to table schema' do
using RSpec::Parameterized::TableSyntax
where(:name, :classification) do
- 'ci_builds' | :gitlab_ci
- 'my_schema.ci_builds' | :gitlab_ci
- 'information_schema.columns' | :gitlab_internal
- 'audit_events_part_5fc467ac26' | :gitlab_main
- '_test_gitlab_main_table' | :gitlab_main
- '_test_gitlab_ci_table' | :gitlab_ci
- '_test_my_table' | :gitlab_shared
- 'pg_attribute' | :gitlab_internal
+ 'ci_builds' | :gitlab_ci
+ 'my_schema.ci_builds' | :gitlab_ci
+ 'my_schema.ci_runner_machine_builds_100' | :gitlab_ci
+ 'my_schema._test_gitlab_main_table' | :gitlab_main
+ 'information_schema.columns' | :gitlab_internal
+ 'audit_events_part_5fc467ac26' | :gitlab_main
+ '_test_gitlab_main_table' | :gitlab_main
+ '_test_gitlab_ci_table' | :gitlab_ci
+ '_test_my_table' | :gitlab_shared
+ 'pg_attribute' | :gitlab_internal
end
with_them do
diff --git a/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
new file mode 100644
index 00000000000..b1971977e7c
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers/convert_to_bigint_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::MigrationHelpers::ConvertToBigint, feature_category: :database do
+ describe 'com_or_dev_or_test_but_not_jh?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:dot_com, :dev_or_test, :jh, :expectation) do
+ true | true | true | false
+ true | false | true | false
+ false | true | true | false
+ false | false | true | false
+ true | true | false | true
+ true | false | false | true
+ false | true | false | true
+ false | false | false | false
+ end
+
+ with_them do
+ it 'returns true for GitLab.com (but not JH), dev, or test' do
+ allow(Gitlab).to receive(:com?).and_return(dot_com)
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test)
+ allow(Gitlab).to receive(:jh?).and_return(jh)
+
+ migration = Class
+ .new
+ .include(Gitlab::Database::MigrationHelpers::ConvertToBigint)
+ .new
+
+ expect(migration.com_or_dev_or_test_but_not_jh?).to eq(expectation)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 9df23776be8..3f6528558b1 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::MigrationHelpers do
+RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database do
include Database::TableSchemaHelpers
include Database::TriggerHelpers
@@ -928,13 +928,13 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'references the custom target columns when provided', :aggregate_failures do
expect(model).to receive(:with_lock_retries).and_yield
expect(model).to receive(:execute).with(
- "ALTER TABLE projects\n" \
- "ADD CONSTRAINT fk_multiple_columns\n" \
- "FOREIGN KEY \(partition_number, user_id\)\n" \
- "REFERENCES users \(partition_number, id\)\n" \
- "ON UPDATE CASCADE\n" \
- "ON DELETE CASCADE\n" \
- "NOT VALID;\n"
+ "ALTER TABLE projects " \
+ "ADD CONSTRAINT fk_multiple_columns " \
+ "FOREIGN KEY \(partition_number, user_id\) " \
+ "REFERENCES users \(partition_number, id\) " \
+ "ON UPDATE CASCADE " \
+ "ON DELETE CASCADE " \
+ "NOT VALID;"
)
model.add_concurrent_foreign_key(
@@ -979,6 +979,80 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ context 'when creating foreign key on a partitioned table' do
+ let(:source) { :_test_source_partitioned_table }
+ let(:dest) { :_test_dest_partitioned_table }
+ let(:args) { [source, dest] }
+ let(:options) { { column: [:partition_id, :owner_id], target_column: [:partition_id, :id] } }
+
+ before do
+ model.execute(<<~SQL)
+ CREATE TABLE public.#{source} (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ owner_id bigint NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ ) PARTITION BY LIST(partition_id);
+
+ CREATE TABLE #{source}_1
+ PARTITION OF public.#{source}
+ FOR VALUES IN (1);
+
+ CREATE TABLE public.#{dest} (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ );
+ SQL
+ end
+
+ it 'creates the FK without using NOT VALID', :aggregate_failures do
+ allow(model).to receive(:execute).and_call_original
+
+ expect(model).to receive(:with_lock_retries).and_yield
+
+ expect(model).to receive(:execute).with(
+ "ALTER TABLE #{source} " \
+ "ADD CONSTRAINT fk_multiple_columns " \
+ "FOREIGN KEY \(partition_id, owner_id\) " \
+ "REFERENCES #{dest} \(partition_id, id\) " \
+ "ON UPDATE CASCADE ON DELETE CASCADE ;"
+ )
+
+ model.add_concurrent_foreign_key(
+ *args,
+ name: :fk_multiple_columns,
+ on_update: :cascade,
+ allow_partitioned: true,
+ **options
+ )
+ end
+
+ it 'raises an error if allow_partitioned is not set' do
+ expect(model).not_to receive(:with_lock_retries).and_yield
+ expect(model).not_to receive(:execute).with(/FOREIGN KEY/)
+
+ expect { model.add_concurrent_foreign_key(*args, **options) }
+ .to raise_error ArgumentError, /use add_concurrent_partitioned_foreign_key/
+ end
+
+ context 'when the reverse_lock_order flag is set' do
+ it 'explicitly locks the tables in target-source order', :aggregate_failures do
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
+
+ expect(model).to receive(:execute).with("LOCK TABLE #{dest}, #{source} IN ACCESS EXCLUSIVE MODE")
+ expect(model).to receive(:execute).with(/REFERENCES #{dest} \(partition_id, id\)/)
+
+ model.add_concurrent_foreign_key(*args, reverse_lock_order: true, allow_partitioned: true, **options)
+ end
+ end
+ end
end
end
@@ -1049,6 +1123,8 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
describe '#foreign_key_exists?' do
let(:referenced_table_name) { '_test_gitlab_main_referenced' }
let(:referencing_table_name) { '_test_gitlab_main_referencing' }
+ let(:schema) { 'public' }
+ let(:identifier) { "#{schema}.#{referencing_table_name}" }
before do
model.connection.execute(<<~SQL)
@@ -1085,6 +1161,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
expect(model.foreign_key_exists?(referencing_table_name, target_table)).to be_truthy
end
+ it 'finds existing foreign_keys by identifier' do
+ expect(model.foreign_key_exists?(identifier, target_table)).to be_truthy
+ end
+
it 'compares by column name if given' do
expect(model.foreign_key_exists?(referencing_table_name, target_table, column: :user_id)).to be_falsey
end
@@ -2890,4 +2970,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it { is_expected.to be_falsey }
end
end
+
+ describe "#table_partitioned?" do
+ subject { model.table_partitioned?(table_name) }
+
+ let(:table_name) { 'p_ci_builds_metadata' }
+
+ it { is_expected.to be_truthy }
+
+ context 'with a non-partitioned table' do
+ let(:table_name) { 'users' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
index 3e249b14f2e..f5ce207773f 100644
--- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
@@ -482,16 +482,46 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
.not_to raise_error
end
- it 'logs a warning when migration does not exist' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
+ context 'when specified migration does not exist' do
+ let(:lab_key) { 'DBLAB_ENVIRONMENT' }
- create(:batched_background_migration, :active, migration_attributes.merge(gitlab_schema: :gitlab_something_else))
+ context 'when DBLAB_ENVIRONMENT is not set' do
+ it 'logs a warning' do
+ stub_env(lab_key, nil)
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
- expect(Gitlab::AppLogger).to receive(:warn)
- .with("Could not find batched background migration for the given configuration: #{configuration}")
+ create(:batched_background_migration, :active, migration_attributes.merge(gitlab_schema: :gitlab_something_else))
- expect { ensure_batched_background_migration_is_finished }
- .not_to raise_error
+ expect(Gitlab::AppLogger).to receive(:warn)
+ .with("Could not find batched background migration for the given configuration: #{configuration}")
+
+ expect { ensure_batched_background_migration_is_finished }
+ .not_to raise_error
+ end
+ end
+
+ context 'when DBLAB_ENVIRONMENT is set' do
+ it 'raises an error' do
+ stub_env(lab_key, 'foo')
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
+
+ create(:batched_background_migration, :active, migration_attributes.merge(gitlab_schema: :gitlab_something_else))
+
+ expect { ensure_batched_background_migration_is_finished }
+ .to raise_error(Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers::NonExistentMigrationError)
+ end
+ end
+ end
+
+ context 'when within transaction' do
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(true)
+ end
+
+ it 'does raise an exception' do
+ expect { ensure_batched_background_migration_is_finished }
+ .to raise_error /`ensure_batched_background_migration_is_finished` cannot be run inside a transaction./
+ end
end
it 'finalizes the migration' do
diff --git a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
index 6848fc85aa1..07d913cf5cc 100644
--- a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
@@ -23,43 +23,46 @@ RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
end
end
- describe '#check_constraint_exists?' do
+ describe '#check_constraint_exists?', :aggregate_failures do
before do
- ActiveRecord::Migration.connection.execute(
- 'ALTER TABLE projects ADD CONSTRAINT check_1 CHECK (char_length(path) <= 5) NOT VALID'
- )
-
- ActiveRecord::Migration.connection.execute(
- 'CREATE SCHEMA new_test_schema'
- )
-
- ActiveRecord::Migration.connection.execute(
- 'CREATE TABLE new_test_schema.projects (id integer, name character varying)'
- )
-
- ActiveRecord::Migration.connection.execute(
- 'ALTER TABLE new_test_schema.projects ADD CONSTRAINT check_2 CHECK (char_length(name) <= 5)'
- )
+ ActiveRecord::Migration.connection.execute(<<~SQL)
+ ALTER TABLE projects ADD CONSTRAINT check_1 CHECK (char_length(path) <= 5) NOT VALID;
+ CREATE SCHEMA new_test_schema;
+ CREATE TABLE new_test_schema.projects (id integer, name character varying);
+ ALTER TABLE new_test_schema.projects ADD CONSTRAINT check_2 CHECK (char_length(name) <= 5);
+ SQL
end
it 'returns true if a constraint exists' do
expect(model)
.to be_check_constraint_exists(:projects, 'check_1')
+
+ expect(described_class)
+ .to be_check_constraint_exists(:projects, 'check_1', connection: model.connection)
end
it 'returns false if a constraint does not exist' do
expect(model)
.not_to be_check_constraint_exists(:projects, 'this_does_not_exist')
+
+ expect(described_class)
+ .not_to be_check_constraint_exists(:projects, 'this_does_not_exist', connection: model.connection)
end
it 'returns false if a constraint with the same name exists in another table' do
expect(model)
.not_to be_check_constraint_exists(:users, 'check_1')
+
+ expect(described_class)
+ .not_to be_check_constraint_exists(:users, 'check_1', connection: model.connection)
end
it 'returns false if a constraint with the same name exists for the same table in another schema' do
expect(model)
.not_to be_check_constraint_exists(:projects, 'check_2')
+
+ expect(described_class)
+ .not_to be_check_constraint_exists(:projects, 'check_2', connection: model.connection)
end
end
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 57c5011590c..6bcefa455cf 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
@@ -48,6 +48,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
let(:result_dir) { Pathname.new(Dir.mktmpdir) }
let(:connection) { base_model.connection }
let(:table_name) { "_test_column_copying" }
+ let(:num_rows_in_table) { 1000 }
let(:from_id) { 0 }
after do
@@ -61,7 +62,7 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
data bigint default 0
);
- insert into #{table_name} (id) select i from generate_series(1, 1000) g(i);
+ insert into #{table_name} (id) select i from generate_series(1, #{num_rows_in_table}) g(i);
SQL
end
@@ -134,6 +135,24 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
expect(calls).not_to be_empty
end
+ it 'samples 1 job with a batch size higher than the table size' do
+ calls = []
+ define_background_migration(migration_name) do |*args|
+ travel 1.minute
+ calls << args
+ end
+
+ queue_migration(migration_name, table_name, :id,
+ job_interval: 5.minutes,
+ batch_size: num_rows_in_table * 2,
+ sub_batch_size: num_rows_in_table * 2)
+
+ described_class.new(result_dir: result_dir, connection: connection,
+ from_id: from_id).run_jobs(for_duration: 3.minutes)
+
+ expect(calls.size).to eq(1)
+ end
+
context 'with multiple jobs to run' do
let(:last_id) do
Gitlab::Database::SharedModel.using_connection(connection) do
diff --git a/spec/lib/gitlab/database/partitioning/ci_sliding_list_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/ci_sliding_list_strategy_spec.rb
new file mode 100644
index 00000000000..f415e892818
--- /dev/null
+++ b/spec/lib/gitlab/database/partitioning/ci_sliding_list_strategy_spec.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Partitioning::CiSlidingListStrategy, feature_category: :database do
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:table_name) { :_test_gitlab_ci_partitioned_test }
+ let(:model) { class_double(ApplicationRecord, table_name: table_name, connection: connection) }
+ let(:next_partition_if) { nil }
+ let(:detach_partition_if) { nil }
+
+ subject(:strategy) do
+ described_class.new(model, :partition,
+ next_partition_if: next_partition_if,
+ detach_partition_if: detach_partition_if)
+ end
+
+ before do
+ next if table_name.to_s.starts_with?('p_')
+
+ connection.execute(<<~SQL)
+ create table #{table_name}
+ (
+ id serial not null,
+ partition_id bigint not null,
+ created_at timestamptz not null,
+ primary key (id, partition_id)
+ )
+ partition by list(partition_id);
+
+ create table #{table_name}_100
+ partition of #{table_name} for values in (100);
+
+ create table #{table_name}_101
+ partition of #{table_name} for values in (101);
+ SQL
+ end
+
+ describe '#current_partitions' do
+ it 'detects both partitions' do
+ expect(strategy.current_partitions).to eq(
+ [
+ Gitlab::Database::Partitioning::SingleNumericListPartition.new(
+ table_name, 100, partition_name: "#{table_name}_100"
+ ),
+ Gitlab::Database::Partitioning::SingleNumericListPartition.new(
+ table_name, 101, partition_name: "#{table_name}_101"
+ )
+ ])
+ end
+ end
+
+ describe '#validate_and_fix' do
+ it 'does not call change_column_default' do
+ expect(strategy.model.connection).not_to receive(:change_column_default)
+
+ strategy.validate_and_fix
+ end
+ end
+
+ describe '#active_partition' do
+ it 'is the partition with the largest value' do
+ expect(strategy.active_partition.value).to eq(101)
+ end
+ end
+
+ describe '#missing_partitions' do
+ context 'when next_partition_if returns true' do
+ let(:next_partition_if) { proc { true } }
+
+ it 'is a partition definition for the next partition in the series' do
+ extra = strategy.missing_partitions
+
+ expect(extra.length).to eq(1)
+ expect(extra.first.value).to eq(102)
+ end
+ end
+
+ context 'when next_partition_if returns false' do
+ let(:next_partition_if) { proc { false } }
+
+ it 'is empty' do
+ expect(strategy.missing_partitions).to be_empty
+ end
+ end
+
+ context 'when there are no partitions for the table' do
+ it 'returns a partition for value 1' do
+ connection.execute("drop table #{table_name}_100; drop table #{table_name}_101;")
+
+ missing_partitions = strategy.missing_partitions
+
+ expect(missing_partitions.size).to eq(1)
+ missing_partition = missing_partitions.first
+
+ expect(missing_partition.value).to eq(100)
+ end
+ end
+ end
+
+ describe '#extra_partitions' do
+ context 'when all partitions are true for detach_partition_if' do
+ let(:detach_partition_if) { ->(_p) { true } }
+
+ it { expect(strategy.extra_partitions).to be_empty }
+ end
+
+ context 'when all partitions are false for detach_partition_if' do
+ let(:detach_partition_if) { proc { false } }
+
+ it { expect(strategy.extra_partitions).to be_empty }
+ end
+ end
+
+ describe '#initial_partition' do
+ it 'starts with the value 100', :aggregate_failures do
+ initial_partition = strategy.initial_partition
+ expect(initial_partition.value).to eq(100)
+ expect(initial_partition.table).to eq(strategy.table_name)
+ expect(initial_partition.partition_name).to eq("#{strategy.table_name}_100")
+ end
+
+ context 'with routing tables' do
+ let(:table_name) { :p_test_gitlab_ci_partitioned_test }
+
+ it 'removes the prefix', :aggregate_failures do
+ initial_partition = strategy.initial_partition
+
+ expect(initial_partition.value).to eq(100)
+ expect(initial_partition.table).to eq(strategy.table_name)
+ expect(initial_partition.partition_name).to eq('test_gitlab_ci_partitioned_test_100')
+ end
+ end
+ end
+
+ describe '#next_partition' do
+ before do
+ allow(strategy)
+ .to receive(:active_partition)
+ .and_return(instance_double(Gitlab::Database::Partitioning::SingleNumericListPartition, value: 105))
+ end
+
+ it 'is one after the active partition', :aggregate_failures do
+ next_partition = strategy.next_partition
+
+ expect(next_partition.value).to eq(106)
+ expect(next_partition.table).to eq(strategy.table_name)
+ expect(next_partition.partition_name).to eq("#{strategy.table_name}_106")
+ end
+
+ context 'with routing tables' do
+ let(:table_name) { :p_test_gitlab_ci_partitioned_test }
+
+ it 'removes the prefix', :aggregate_failures do
+ next_partition = strategy.next_partition
+
+ expect(next_partition.value).to eq(106)
+ expect(next_partition.table).to eq(strategy.table_name)
+ expect(next_partition.partition_name).to eq('test_gitlab_ci_partitioned_test_106')
+ end
+ end
+ end
+
+ describe '#ensure_partitioning_column_ignored_or_readonly!' do
+ it 'does not raise when the column is not ignored' do
+ expect do
+ Class.new(ApplicationRecord) do
+ include PartitionedTable
+
+ partitioned_by :partition_id,
+ strategy: :ci_sliding_list,
+ next_partition_if: proc { false },
+ detach_partition_if: proc { false }
+ end
+ end.not_to raise_error
+ 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 2212cb09888..ac54c307108 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
sync_partitions
end
- context 'with eplicitly provided connection' do
+ context 'with explicitly provided connection' do
let(:connection) { Ci::ApplicationRecord.connection }
it 'uses the explicitly provided connection when any' do
@@ -59,6 +59,14 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
end
+ context 'when an ArgumentError occurs during partition management' do
+ it 'raises error' do
+ expect(partitioning_strategy).to receive(:missing_partitions).and_raise(ArgumentError)
+
+ expect { sync_partitions }.to raise_error(ArgumentError)
+ 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)')
@@ -230,23 +238,20 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
expect(pending_drop.drop_after).to eq(Time.current + described_class::RETAIN_DETACHED_PARTITIONS_FOR)
end
- # Postgres 11 does not support foreign keys to partitioned tables
- if ApplicationRecord.database.version.to_f >= 12
- context 'when the model is the target of a foreign key' do
- before do
- connection.execute(<<~SQL)
+ context 'when the model is the target of a foreign key' do
+ before do
+ connection.execute(<<~SQL)
create unique index idx_for_fk ON #{partitioned_table_name}(created_at);
create table _test_gitlab_main_referencing_table (
id bigserial primary key not null,
referencing_created_at timestamptz references #{partitioned_table_name}(created_at)
);
- SQL
- end
+ SQL
+ end
- it 'does not detach partitions with a referenced foreign key' do
- expect { subject }.not_to change { find_partitions(my_model.table_name).size }
- end
+ it 'does not detach partitions with a referenced foreign key' do
+ expect { subject }.not_to change { find_partitions(my_model.table_name).size }
end
end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
index f0e34476cf2..d5f4afd7ba4 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers do
+RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers, feature_category: :database do
include Database::TableSchemaHelpers
let(:migration) do
@@ -16,15 +16,23 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
let(:partition_schema) { 'gitlab_partitions_dynamic' }
let(:partition1_name) { "#{partition_schema}.#{source_table_name}_202001" }
let(:partition2_name) { "#{partition_schema}.#{source_table_name}_202002" }
+ let(:validate) { true }
let(:options) do
{
column: column_name,
name: foreign_key_name,
on_delete: :cascade,
- validate: true
+ on_update: nil,
+ primary_key: :id
}
end
+ let(:create_options) do
+ options
+ .except(:primary_key)
+ .merge!(reverse_lock_order: false, target_column: :id, validate: validate)
+ end
+
before do
allow(migration).to receive(:puts)
allow(migration).to receive(:transaction_open?).and_return(false)
@@ -67,12 +75,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name)
- expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **options)
- expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **options)
+ expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **create_options)
+ expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **create_options)
- expect(migration).to receive(:with_lock_retries).ordered.and_yield
- expect(migration).to receive(:add_foreign_key)
- .with(source_table_name, target_table_name, **options)
+ expect(migration).to receive(:add_concurrent_foreign_key)
+ .with(source_table_name, target_table_name, allow_partitioned: true, **create_options)
.ordered
.and_call_original
@@ -81,6 +88,39 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
expect_foreign_key_to_exist(source_table_name, foreign_key_name)
end
+ context 'with validate: false option' do
+ let(:validate) { false }
+ let(:options) do
+ {
+ column: column_name,
+ name: foreign_key_name,
+ on_delete: :cascade,
+ on_update: nil,
+ primary_key: :id
+ }
+ end
+
+ it 'creates the foreign key only on partitions' do
+ expect(migration).to receive(:foreign_key_exists?)
+ .with(source_table_name, target_table_name, **options)
+ .and_return(false)
+
+ expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name)
+
+ expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **create_options)
+ expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **create_options)
+
+ expect(migration).not_to receive(:add_concurrent_foreign_key)
+ .with(source_table_name, target_table_name, **create_options)
+
+ migration.add_concurrent_partitioned_foreign_key(
+ source_table_name, target_table_name,
+ column: column_name, validate: false)
+
+ expect_foreign_key_not_to_exist(source_table_name, foreign_key_name)
+ end
+ end
+
def expect_add_concurrent_fk_and_call_original(source_table_name, target_table_name, options)
expect(migration).to receive(:add_concurrent_foreign_key)
.ordered
@@ -100,8 +140,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
.and_return(true)
expect(migration).not_to receive(:add_concurrent_foreign_key)
- expect(migration).not_to receive(:with_lock_retries)
- expect(migration).not_to receive(:add_foreign_key)
migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, column: column_name)
@@ -110,30 +148,43 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
end
context 'when additional foreign key options are given' do
- let(:options) do
+ let(:exits_options) do
{
column: column_name,
name: '_my_fk_name',
on_delete: :restrict,
- validate: true
+ on_update: nil,
+ primary_key: :id
}
end
+ let(:create_options) do
+ exits_options
+ .except(:primary_key)
+ .merge!(reverse_lock_order: false, target_column: :id, validate: true)
+ end
+
it 'forwards them to the foreign key helper methods' do
expect(migration).to receive(:foreign_key_exists?)
- .with(source_table_name, target_table_name, **options)
+ .with(source_table_name, target_table_name, **exits_options)
.and_return(false)
expect(migration).not_to receive(:concurrent_partitioned_foreign_key_name)
- expect_add_concurrent_fk(partition1_name, target_table_name, **options)
- expect_add_concurrent_fk(partition2_name, target_table_name, **options)
+ expect_add_concurrent_fk(partition1_name, target_table_name, **create_options)
+ expect_add_concurrent_fk(partition2_name, target_table_name, **create_options)
- expect(migration).to receive(:with_lock_retries).ordered.and_yield
- expect(migration).to receive(:add_foreign_key).with(source_table_name, target_table_name, **options).ordered
+ expect(migration).to receive(:add_concurrent_foreign_key)
+ .with(source_table_name, target_table_name, allow_partitioned: true, **create_options)
+ .ordered
- migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name,
- column: column_name, name: '_my_fk_name', on_delete: :restrict)
+ migration.add_concurrent_partitioned_foreign_key(
+ source_table_name,
+ target_table_name,
+ column: column_name,
+ name: '_my_fk_name',
+ on_delete: :restrict
+ )
end
def expect_add_concurrent_fk(source_table_name, target_table_name, options)
@@ -153,4 +204,39 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
end
end
end
+
+ describe '#validate_partitioned_foreign_key' do
+ context 'when run inside a transaction block' do
+ it 'raises an error' do
+ expect(migration).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ migration.validate_partitioned_foreign_key(source_table_name, column_name, name: '_my_fk_name')
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+
+ context 'when run outside a transaction block' do
+ before do
+ migration.add_concurrent_partitioned_foreign_key(
+ source_table_name,
+ target_table_name,
+ column: column_name,
+ name: foreign_key_name,
+ validate: false
+ )
+ end
+
+ it 'validates FK for each partition' do
+ expect(migration).to receive(:execute).with(/SET statement_timeout TO 0/).twice
+ expect(migration).to receive(:execute).with(/RESET statement_timeout/).twice
+ expect(migration).to receive(:execute)
+ .with(/ALTER TABLE #{partition1_name} VALIDATE CONSTRAINT #{foreign_key_name}/).ordered
+ expect(migration).to receive(:execute)
+ .with(/ALTER TABLE #{partition2_name} VALIDATE CONSTRAINT #{foreign_key_name}/).ordered
+
+ migration.validate_partitioned_foreign_key(source_table_name, column_name, name: foreign_key_name)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb
index ae74ee60a4b..4c0fde46b2f 100644
--- a/spec/lib/gitlab/database/partitioning_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_spec.rb
@@ -67,14 +67,19 @@ RSpec.describe Gitlab::Database::Partitioning do
let(:ci_connection) { Ci::ApplicationRecord.connection }
let(:table_names) { %w[partitioning_test1 partitioning_test2] }
let(:models) do
- table_names.map do |table_name|
+ [
Class.new(ApplicationRecord) do
include PartitionedTable
- self.table_name = table_name
+ self.table_name = 'partitioning_test1'
partitioned_by :created_at, strategy: :monthly
+ end,
+ Class.new(Gitlab::Database::Partitioning::TableWithoutModel).tap do |klass|
+ klass.table_name = 'partitioning_test2'
+ klass.partitioned_by(:created_at, strategy: :monthly)
+ klass.limit_connection_names = %i[main]
end
- end
+ ]
end
before do
diff --git a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
index ae56f66737d..c128c56c708 100644
--- a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
+++ b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
@@ -70,13 +70,29 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
end
describe '#by_constrained_table_name' do
- it 'finds the foreign keys for the constrained table' do
- expected = described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a
+ let(:expected) { described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a }
+ it 'finds the foreign keys for the constrained table' do
expect(described_class.by_constrained_table_name(table_name("constrained_table"))).to match_array(expected)
end
end
+ describe '#by_constrained_table_name_or_identifier' do
+ let(:expected) { described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a }
+
+ context 'when using table name' do
+ it 'finds the foreign keys for the constrained table' do
+ expect(described_class.by_constrained_table_name_or_identifier(table_name("constrained_table"))).to match_array(expected)
+ end
+ end
+
+ context 'when using identifier' do
+ it 'finds the foreign keys for the constrained table' do
+ expect(described_class.by_constrained_table_name_or_identifier(schema_table_name('constrained_table'))).to match_array(expected)
+ end
+ end
+ end
+
describe '#by_name' do
it 'finds foreign keys by name' do
expect(described_class.by_name('fk_constrained_to_referenced').pluck(:name)).to contain_exactly('fk_constrained_to_referenced')
@@ -187,10 +203,8 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
end
end
- context 'when supporting foreign keys to inherited tables in postgres 12' do
+ context 'when supporting foreign keys to inherited tables' do
before do
- skip('not supported before postgres 12') if ApplicationRecord.database.version.to_f < 12
-
ApplicationRecord.connection.execute(<<~SQL)
create table #{schema_table_name('parent')} (
id bigserial primary key not null
diff --git a/spec/lib/gitlab/database/postgres_partition_spec.rb b/spec/lib/gitlab/database/postgres_partition_spec.rb
index 14a4d405621..48dbdbc7757 100644
--- a/spec/lib/gitlab/database/postgres_partition_spec.rb
+++ b/spec/lib/gitlab/database/postgres_partition_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::PostgresPartition, type: :model do
+RSpec.describe Gitlab::Database::PostgresPartition, type: :model, feature_category: :database do
+ let(:current_schema) { ActiveRecord::Base.connection.select_value("SELECT current_schema()") }
let(:schema) { 'gitlab_partitions_dynamic' }
let(:name) { '_test_partition_01' }
let(:identifier) { "#{schema}.#{name}" }
@@ -56,9 +57,20 @@ RSpec.describe Gitlab::Database::PostgresPartition, type: :model do
expect(partitions.pluck(:name)).to eq([name, second_name])
end
+ it 'returns the partitions if the parent table schema is included in the table name' do
+ partitions = described_class.for_parent_table("#{current_schema}._test_partitioned_table")
+
+ expect(partitions.count).to eq(2)
+ expect(partitions.pluck(:name)).to eq([name, second_name])
+ end
+
it 'does not return partitions for tables not in the current schema' do
expect(described_class.for_parent_table('_test_other_table').count).to eq(0)
end
+
+ it 'does not return partitions for tables if the schema is not the current' do
+ expect(described_class.for_parent_table('foo_bar._test_partitioned_table').count).to eq(0)
+ end
end
describe '#parent_identifier' do
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index a8af9bb5a38..4d0e58b0937 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
context 'when async FK validation is enabled' do
it 'executes FK validation for each database prior to any reindexing actions' do
- expect(Gitlab::Database::AsyncForeignKeys).to receive(:validate_pending_entries!).ordered.exactly(databases_count).times
+ expect(Gitlab::Database::AsyncConstraints).to receive(:validate_pending_entries!).ordered.exactly(databases_count).times
expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times
described_class.invoke
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
it 'does not execute FK validation' do
stub_feature_flags(database_async_foreign_key_validation: false)
- expect(Gitlab::Database::AsyncForeignKeys).not_to receive(:validate_pending_entries!)
+ expect(Gitlab::Database::AsyncConstraints).not_to receive(:validate_pending_entries!)
described_class.invoke
end
diff --git a/spec/lib/gitlab/database/schema_validation/database_spec.rb b/spec/lib/gitlab/database/schema_validation/database_spec.rb
index c0026f91b46..eadaf683a29 100644
--- a/spec/lib/gitlab/database/schema_validation/database_spec.rb
+++ b/spec/lib/gitlab/database/schema_validation/database_spec.rb
@@ -3,43 +3,108 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do
- let(:database_name) { 'main' }
- let(:database_indexes) do
- [['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
- end
+ subject(:database) { described_class.new(connection) }
- let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
- let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:database_model) { Gitlab::Database.database_base_models['main'] }
let(:connection) { database_model.connection }
- subject(:database) { described_class.new(connection) }
+ context 'when having indexes' do
+ let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Index }
+ let(:results) do
+ [['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
+ end
- before do
- allow(connection).to receive(:exec_query).and_return(query_result)
- end
+ before do
+ allow(connection).to receive(:select_rows).and_return(results)
+ end
+
+ describe '#fetch_index_by_name' do
+ context 'when index does not exist' do
+ it 'returns nil' do
+ index = database.fetch_index_by_name('non_existing_index')
+
+ expect(index).to be_nil
+ end
+ end
+
+ it 'returns index by name' do
+ index = database.fetch_index_by_name('index')
+
+ expect(index.name).to eq('index')
+ end
+ end
+
+ describe '#index_exists?' do
+ context 'when index exists' do
+ it 'returns true' do
+ index_exists = database.index_exists?('index')
+
+ expect(index_exists).to be_truthy
+ end
+ end
- describe '#fetch_index_by_name' do
- context 'when index does not exist' do
- it 'returns nil' do
- index = database.fetch_index_by_name('non_existing_index')
+ context 'when index does not exist' do
+ it 'returns false' do
+ index_exists = database.index_exists?('non_existing_index')
- expect(index).to be_nil
+ expect(index_exists).to be_falsey
+ end
end
end
- it 'returns index by name' do
- index = database.fetch_index_by_name('index')
+ describe '#indexes' do
+ it 'returns indexes' do
+ indexes = database.indexes
- expect(index.name).to eq('index')
+ expect(indexes).to all(be_a(schema_object))
+ expect(indexes.map(&:name)).to eq(['index'])
+ end
end
end
- describe '#indexes' do
- it 'returns indexes' do
- indexes = database.indexes
+ context 'when having triggers' do
+ let(:schema_object) { Gitlab::Database::SchemaValidation::SchemaObjects::Trigger }
+ let(:results) do
+ { 'my_trigger' => 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' }
+ end
+
+ before do
+ allow(database).to receive(:fetch_triggers).and_return(results)
+ end
+
+ describe '#fetch_trigger_by_name' do
+ context 'when trigger does not exist' do
+ it 'returns nil' do
+ expect(database.fetch_trigger_by_name('non_existing_trigger')).to be_nil
+ end
+ end
+
+ it 'returns trigger by name' do
+ expect(database.fetch_trigger_by_name('my_trigger').name).to eq('my_trigger')
+ end
+ end
+
+ describe '#trigger_exists?' do
+ context 'when trigger exists' do
+ it 'returns true' do
+ expect(database.trigger_exists?('my_trigger')).to be_truthy
+ end
+ end
+
+ context 'when trigger does not exist' do
+ it 'returns false' do
+ expect(database.trigger_exists?('non_existing_trigger')).to be_falsey
+ end
+ end
+ end
- expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
- expect(indexes.map(&:name)).to eq(['index'])
+ describe '#triggers' do
+ it 'returns triggers' do
+ triggers = database.triggers
+
+ expect(triggers).to all(be_a(schema_object))
+ expect(triggers.map(&:name)).to eq(['my_trigger'])
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/schema_validation/index_spec.rb b/spec/lib/gitlab/database/schema_validation/index_spec.rb
deleted file mode 100644
index 297211d79ed..00000000000
--- a/spec/lib/gitlab/database/schema_validation/index_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Index, feature_category: :database do
- let(:index_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
-
- let(:stmt) { PgQuery.parse(index_statement).tree.stmts.first.stmt.index_stmt }
-
- let(:index) { described_class.new(stmt) }
-
- describe '#name' do
- it 'returns index name' do
- expect(index.name).to eq('index_name')
- end
- end
-
- describe '#statement' do
- it 'returns index statement' do
- expect(index.statement).to eq(index_statement)
- end
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
deleted file mode 100644
index 4351031a4b4..00000000000
--- a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Database::SchemaValidation::Indexes, feature_category: :database do
- let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
- let(:database_indexes) do
- [
- ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'],
- ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'],
- ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']
- ]
- end
-
- let(:database_name) { 'main' }
-
- let(:database_model) { Gitlab::Database.database_base_models[database_name] }
-
- let(:connection) { database_model.connection }
-
- let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
-
- let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
- let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
-
- subject(:schema_validation) { described_class.new(structure_file, database) }
-
- before do
- allow(connection).to receive(:exec_query).and_return(query_result)
- end
-
- describe '#missing_indexes' do
- it 'returns missing indexes' do
- missing_indexes = %w[
- missing_index
- index_namespaces_public_groups_name_id
- index_on_deploy_keys_id_and_type_and_public
- index_users_on_public_email_excluding_null_and_empty
- ]
-
- expect(schema_validation.missing_indexes).to match_array(missing_indexes)
- end
- end
-
- describe '#extra_indexes' do
- it 'returns extra indexes' do
- expect(schema_validation.extra_indexes).to match_array(['extra_index'])
- end
- end
-
- describe '#wrong_indexes' do
- it 'returns wrong indexes' do
- expect(schema_validation.wrong_indexes).to match_array(['wrong_index'])
- end
- end
-end
diff --git a/spec/lib/gitlab/database/schema_validation/runner_spec.rb b/spec/lib/gitlab/database/schema_validation/runner_spec.rb
new file mode 100644
index 00000000000..ddbdedcd8b4
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/runner_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Runner, feature_category: :database do
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:connection) { ActiveRecord::Base.connection }
+
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+ let(:structure_sql) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, 'public') }
+
+ describe '#execute' do
+ subject(:inconsistencies) { described_class.new(structure_sql, database).execute }
+
+ it 'returns inconsistencies' do
+ expect(inconsistencies).not_to be_empty
+ end
+
+ it 'execute all validators' do
+ all_validators = Gitlab::Database::SchemaValidation::Validators::BaseValidator.all_validators
+
+ expect(all_validators).to all(receive(:new).with(structure_sql, database).and_call_original)
+
+ inconsistencies
+ end
+
+ context 'when validators are passed' do
+ subject(:inconsistencies) { described_class.new(structure_sql, database, validators: validators).execute }
+
+ let(:class_name) { 'Gitlab::Database::SchemaValidation::Validators::ExtraIndexes' }
+ let(:inconsistency_class_name) { 'Gitlab::Database::SchemaValidation::Validators::BaseValidator::Inconsistency' }
+
+ let(:extra_indexes) { class_double(class_name) }
+ let(:instace_extra_index) { instance_double(class_name, execute: [inconsistency]) }
+ let(:inconsistency) { instance_double(inconsistency_class_name, object_name: 'test') }
+
+ let(:validators) { [extra_indexes] }
+
+ it 'only execute the validators passed' do
+ expect(extra_indexes).to receive(:new).with(structure_sql, database).and_return(instace_extra_index)
+
+ Gitlab::Database::SchemaValidation::Validators::BaseValidator.all_validators.each do |validator|
+ expect(validator).not_to receive(:new).with(structure_sql, database)
+ end
+
+ expect(inconsistencies.map(&:object_name)).to eql ['test']
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb
new file mode 100644
index 00000000000..1aaa994e3bb
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/schema_objects/index_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Index, feature_category: :database do
+ let(:statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
+ let(:name) { 'index_name' }
+
+ include_examples 'schema objects assertions for', 'index_stmt'
+end
diff --git a/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb b/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb
new file mode 100644
index 00000000000..8000a54ee27
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/schema_objects/trigger_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::SchemaObjects::Trigger, feature_category: :database do
+ let(:statement) { 'CREATE TRIGGER my_trigger BEFORE INSERT ON todos FOR EACH ROW EXECUTE FUNCTION trigger()' }
+ let(:name) { 'my_trigger' }
+
+ include_examples 'schema objects assertions for', 'create_trig_stmt'
+end
diff --git a/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb b/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb
new file mode 100644
index 00000000000..cc0bd4125ef
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/structure_sql_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::StructureSql, feature_category: :database do
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:schema_name) { 'public' }
+
+ subject(:structure_sql) { described_class.new(structure_file_path, schema_name) }
+
+ context 'when having indexes' do
+ describe '#index_exists?' do
+ subject(:index_exists) { structure_sql.index_exists?(index_name) }
+
+ context 'when the index does not exist' do
+ let(:index_name) { 'non-existent-index' }
+
+ it 'returns false' do
+ expect(index_exists).to be_falsey
+ end
+ end
+
+ context 'when the index exists' do
+ let(:index_name) { 'index' }
+
+ it 'returns true' do
+ expect(index_exists).to be_truthy
+ end
+ end
+ end
+
+ describe '#indexes' do
+ it 'returns indexes' do
+ indexes = structure_sql.indexes
+
+ expected_indexes = %w[
+ missing_index
+ wrong_index
+ index
+ index_namespaces_public_groups_name_id
+ index_on_deploy_keys_id_and_type_and_public
+ index_users_on_public_email_excluding_null_and_empty
+ ]
+
+ expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::SchemaObjects::Index))
+ expect(indexes.map(&:name)).to eq(expected_indexes)
+ end
+ end
+ end
+
+ context 'when having triggers' do
+ describe '#trigger_exists?' do
+ subject(:trigger_exists) { structure_sql.trigger_exists?(name) }
+
+ context 'when the trigger does not exist' do
+ let(:name) { 'non-existent-trigger' }
+
+ it 'returns false' do
+ expect(trigger_exists).to be_falsey
+ end
+ end
+
+ context 'when the trigger exists' do
+ let(:name) { 'trigger' }
+
+ it 'returns true' do
+ expect(trigger_exists).to be_truthy
+ end
+ end
+ end
+
+ describe '#triggers' do
+ it 'returns triggers' do
+ triggers = structure_sql.triggers
+ expected_triggers = %w[trigger wrong_trigger missing_trigger_1 projects_loose_fk_trigger]
+
+ expect(triggers).to all(be_a(Gitlab::Database::SchemaValidation::SchemaObjects::Trigger))
+ expect(triggers.map(&:name)).to eq(expected_triggers)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb
new file mode 100644
index 00000000000..2f38c25cf68
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/base_validator_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::BaseValidator, feature_category: :database do
+ describe '.all_validators' do
+ subject(:all_validators) { described_class.all_validators }
+
+ it 'returns an array of all validators' do
+ expect(all_validators).to eq([
+ Gitlab::Database::SchemaValidation::Validators::ExtraIndexes,
+ Gitlab::Database::SchemaValidation::Validators::ExtraTriggers,
+ Gitlab::Database::SchemaValidation::Validators::MissingIndexes,
+ Gitlab::Database::SchemaValidation::Validators::MissingTriggers,
+ Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes,
+ Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers
+ ])
+ end
+ end
+
+ describe '#execute' do
+ let(:structure_sql) { instance_double(Gitlab::Database::SchemaValidation::StructureSql) }
+ let(:database) { instance_double(Gitlab::Database::SchemaValidation::Database) }
+
+ subject(:inconsistencies) { described_class.new(structure_sql, database).execute }
+
+ it 'raises an exception' do
+ expect { inconsistencies }.to raise_error(NoMethodError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/different_definition_indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/different_definition_indexes_spec.rb
new file mode 100644
index 00000000000..b9744c86b80
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/different_definition_indexes_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionIndexes,
+ feature_category: :database do
+ include_examples 'index validators', described_class, ['wrong_index']
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb
new file mode 100644
index 00000000000..4d065929708
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/different_definition_triggers_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::DifferentDefinitionTriggers,
+ feature_category: :database do
+ include_examples 'trigger validators', described_class, ['wrong_trigger']
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/extra_indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/extra_indexes_spec.rb
new file mode 100644
index 00000000000..842dbb42120
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/extra_indexes_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraIndexes, feature_category: :database do
+ include_examples 'index validators', described_class, ['extra_index']
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb
new file mode 100644
index 00000000000..d2e1c18a1ab
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/extra_triggers_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::ExtraTriggers, feature_category: :database do
+ include_examples 'trigger validators', described_class, ['extra_trigger']
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/missing_indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/missing_indexes_spec.rb
new file mode 100644
index 00000000000..c402c3a2fa7
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/missing_indexes_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingIndexes, feature_category: :database do
+ missing_indexes = %w[
+ missing_index
+ index_namespaces_public_groups_name_id
+ index_on_deploy_keys_id_and_type_and_public
+ index_users_on_public_email_excluding_null_and_empty
+ ]
+
+ include_examples 'index validators', described_class, missing_indexes
+end
diff --git a/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb b/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb
new file mode 100644
index 00000000000..87bc3ded808
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/validators/missing_triggers_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Validators::MissingTriggers, feature_category: :database do
+ missing_triggers = %w[missing_trigger_1 projects_loose_fk_trigger]
+
+ include_examples 'trigger validators', described_class, missing_triggers
+end
diff --git a/spec/lib/gitlab/database/tables_locker_spec.rb b/spec/lib/gitlab/database/tables_locker_spec.rb
index d74f455eaad..30f0f9376c8 100644
--- a/spec/lib/gitlab/database/tables_locker_spec.rb
+++ b/spec/lib/gitlab/database/tables_locker_spec.rb
@@ -2,20 +2,38 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base, :delete, :silence_stdout,
- :suppress_gitlab_schemas_validate_connection, feature_category: :pods do
- let(:detached_partition_table) { '_test_gitlab_main_part_20220101' }
- let(:lock_writes_manager) do
+RSpec.describe Gitlab::Database::TablesLocker, :suppress_gitlab_schemas_validate_connection, :silence_stdout,
+ feature_category: :pods do
+ let(:default_lock_writes_manager) do
instance_double(Gitlab::Database::LockWritesManager, lock_writes: nil, unlock_writes: nil)
end
before do
- allow(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
+ allow(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(default_lock_writes_manager)
+ # Limiting the scope of the tests to a subset of the database tables
+ allow(Gitlab::Database::GitlabSchema).to receive(:tables_to_schema).and_return({
+ 'application_setttings' => :gitlab_main_clusterwide,
+ 'projects' => :gitlab_main,
+ 'security_findings' => :gitlab_main,
+ 'ci_builds' => :gitlab_ci,
+ 'ci_jobs' => :gitlab_ci,
+ 'loose_foreign_keys_deleted_records' => :gitlab_shared,
+ 'ar_internal_metadata' => :gitlab_internal
+ })
end
before(:all) do
+ create_partition_sql = <<~SQL
+ CREATE TABLE IF NOT EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.security_findings_test_partition
+ PARTITION OF security_findings
+ FOR VALUES IN (0)
+ SQL
+
+ ApplicationRecord.connection.execute(create_partition_sql)
+ Ci::ApplicationRecord.connection.execute(create_partition_sql)
+
create_detached_partition_sql = <<~SQL
- CREATE TABLE IF NOT EXISTS gitlab_partitions_dynamic._test_gitlab_main_part_20220101 (
+ CREATE TABLE IF NOT EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201 (
id bigserial primary key not null
)
SQL
@@ -29,35 +47,81 @@ RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base
drop_after: Time.current
)
end
+ Gitlab::Database::SharedModel.using_connection(Ci::ApplicationRecord.connection) do
+ Postgresql::DetachedPartition.create!(
+ table_name: '_test_gitlab_main_part_20220101',
+ drop_after: Time.current
+ )
+ end
end
- after(:all) do
- drop_detached_partition_sql = <<~SQL
- DROP TABLE IF EXISTS gitlab_partitions_dynamic._test_gitlab_main_part_20220101
- SQL
+ shared_examples "lock tables" do |gitlab_schema, database_name|
+ let(:connection) { Gitlab::Database.database_base_models[database_name].connection }
+ let(:tables_to_lock) do
+ Gitlab::Database::GitlabSchema
+ .tables_to_schema.filter_map { |table_name, schema| table_name if schema == gitlab_schema }
+ end
- ApplicationRecord.connection.execute(drop_detached_partition_sql)
- Ci::ApplicationRecord.connection.execute(drop_detached_partition_sql)
+ it "locks table in schema #{gitlab_schema} and database #{database_name}" do
+ expect(tables_to_lock).not_to be_empty
- Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do
- Postgresql::DetachedPartition.delete_all
+ tables_to_lock.each do |table_name|
+ lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, lock_writes: nil)
+
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
+ table_name: table_name,
+ connection: connection,
+ database_name: database_name,
+ with_retries: true,
+ logger: anything,
+ dry_run: anything
+ ).once.and_return(lock_writes_manager)
+ expect(lock_writes_manager).to receive(:lock_writes).once
+ end
+
+ subject
end
end
- shared_examples "lock tables" do |table_schema, database_name|
- let(:table_name) do
+ shared_examples "unlock tables" do |gitlab_schema, database_name|
+ let(:connection) { Gitlab::Database.database_base_models[database_name].connection }
+
+ let(:tables_to_unlock) do
Gitlab::Database::GitlabSchema
- .tables_to_schema.filter_map { |table_name, schema| table_name if schema == table_schema }
- .first
+ .tables_to_schema.filter_map { |table_name, schema| table_name if schema == gitlab_schema }
+ end
+
+ it "unlocks table in schema #{gitlab_schema} and database #{database_name}" do
+ expect(tables_to_unlock).not_to be_empty
+
+ tables_to_unlock.each do |table_name|
+ lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, unlock_writes: nil)
+
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
+ table_name: table_name,
+ connection: anything,
+ database_name: database_name,
+ with_retries: true,
+ logger: anything,
+ dry_run: anything
+ ).once.and_return(lock_writes_manager)
+ expect(lock_writes_manager).to receive(:unlock_writes)
+ end
+
+ subject
end
+ end
+
+ shared_examples "lock partitions" do |partition_identifier, database_name|
+ let(:connection) { Gitlab::Database.database_base_models[database_name].connection }
- let(:database) { database_name }
+ it 'locks the partition' do
+ lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, lock_writes: nil)
- it "locks table in schema #{table_schema} and database #{database_name}" do
expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
- table_name: table_name,
- connection: anything,
- database_name: database,
+ table_name: partition_identifier,
+ connection: connection,
+ database_name: database_name,
with_retries: true,
logger: anything,
dry_run: anything
@@ -68,20 +132,16 @@ RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base
end
end
- shared_examples "unlock tables" do |table_schema, database_name|
- let(:table_name) do
- Gitlab::Database::GitlabSchema
- .tables_to_schema.filter_map { |table_name, schema| table_name if schema == table_schema }
- .first
- end
+ shared_examples "unlock partitions" do |partition_identifier, database_name|
+ let(:connection) { Gitlab::Database.database_base_models[database_name].connection }
- let(:database) { database_name }
+ it 'unlocks the partition' do
+ lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, unlock_writes: nil)
- it "unlocks table in schema #{table_schema} and database #{database_name}" do
expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
- table_name: table_name,
- connection: anything,
- database_name: database,
+ table_name: partition_identifier,
+ connection: connection,
+ database_name: database_name,
with_retries: true,
logger: anything,
dry_run: anything
@@ -100,25 +160,29 @@ RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base
describe '#lock_writes' do
subject { described_class.new.lock_writes }
- it 'does not call Gitlab::Database::LockWritesManager.lock_writes' do
- expect(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
- expect(lock_writes_manager).not_to receive(:lock_writes)
+ it 'does not lock any table' do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new)
+ .with(any_args).and_return(default_lock_writes_manager)
+ expect(default_lock_writes_manager).not_to receive(:lock_writes)
subject
end
- include_examples "unlock tables", :gitlab_main, 'main'
- include_examples "unlock tables", :gitlab_ci, 'ci'
- include_examples "unlock tables", :gitlab_shared, 'main'
- include_examples "unlock tables", :gitlab_internal, 'main'
+ it_behaves_like 'unlock tables', :gitlab_main, 'main'
+ it_behaves_like 'unlock tables', :gitlab_ci, 'main'
+ it_behaves_like 'unlock tables', :gitlab_main_clusterwide, 'main'
+ it_behaves_like 'unlock tables', :gitlab_shared, 'main'
+ it_behaves_like 'unlock tables', :gitlab_internal, 'main'
end
describe '#unlock_writes' do
subject { described_class.new.lock_writes }
it 'does call Gitlab::Database::LockWritesManager.unlock_writes' do
- expect(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
- expect(lock_writes_manager).to receive(:unlock_writes)
+ expect(Gitlab::Database::LockWritesManager).to receive(:new)
+ .with(any_args).and_return(default_lock_writes_manager)
+ expect(default_lock_writes_manager).to receive(:unlock_writes)
+ expect(default_lock_writes_manager).not_to receive(:lock_writes)
subject
end
@@ -133,43 +197,61 @@ RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base
describe '#lock_writes' do
subject { described_class.new.lock_writes }
- include_examples "lock tables", :gitlab_ci, 'main'
- include_examples "lock tables", :gitlab_main, 'ci'
-
- include_examples "unlock tables", :gitlab_main, 'main'
- include_examples "unlock tables", :gitlab_ci, 'ci'
- include_examples "unlock tables", :gitlab_shared, 'main'
- include_examples "unlock tables", :gitlab_shared, 'ci'
- include_examples "unlock tables", :gitlab_internal, 'main'
- include_examples "unlock tables", :gitlab_internal, 'ci'
+ it_behaves_like 'lock tables', :gitlab_ci, 'main'
+ it_behaves_like 'lock tables', :gitlab_main, 'ci'
+ it_behaves_like 'lock tables', :gitlab_main_clusterwide, 'ci'
+
+ it_behaves_like 'unlock tables', :gitlab_main_clusterwide, 'main'
+ it_behaves_like 'unlock tables', :gitlab_main, 'main'
+ it_behaves_like 'unlock tables', :gitlab_ci, 'ci'
+ it_behaves_like 'unlock tables', :gitlab_shared, 'main'
+ it_behaves_like 'unlock tables', :gitlab_shared, 'ci'
+ it_behaves_like 'unlock tables', :gitlab_internal, 'main'
+ it_behaves_like 'unlock tables', :gitlab_internal, 'ci'
+
+ gitlab_main_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.security_findings_test_partition"
+ it_behaves_like 'unlock partitions', gitlab_main_partition, 'main'
+ it_behaves_like 'lock partitions', gitlab_main_partition, 'ci'
+
+ gitlab_main_detached_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_20220101"
+ it_behaves_like 'unlock partitions', gitlab_main_detached_partition, 'main'
+ it_behaves_like 'lock partitions', gitlab_main_detached_partition, 'ci'
end
describe '#unlock_writes' do
subject { described_class.new.unlock_writes }
- include_examples "unlock tables", :gitlab_ci, 'main'
- include_examples "unlock tables", :gitlab_main, 'ci'
- include_examples "unlock tables", :gitlab_main, 'main'
- include_examples "unlock tables", :gitlab_ci, 'ci'
- include_examples "unlock tables", :gitlab_shared, 'main'
- include_examples "unlock tables", :gitlab_shared, 'ci'
- include_examples "unlock tables", :gitlab_internal, 'main'
- include_examples "unlock tables", :gitlab_internal, 'ci'
+ it_behaves_like "unlock tables", :gitlab_ci, 'main'
+ it_behaves_like "unlock tables", :gitlab_main, 'ci'
+ it_behaves_like "unlock tables", :gitlab_main, 'main'
+ it_behaves_like "unlock tables", :gitlab_ci, 'ci'
+ it_behaves_like "unlock tables", :gitlab_shared, 'main'
+ it_behaves_like "unlock tables", :gitlab_shared, 'ci'
+ it_behaves_like "unlock tables", :gitlab_internal, 'main'
+ it_behaves_like "unlock tables", :gitlab_internal, 'ci'
+
+ gitlab_main_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.security_findings_test_partition"
+ it_behaves_like 'unlock partitions', gitlab_main_partition, 'main'
+ it_behaves_like 'unlock partitions', gitlab_main_partition, 'ci'
+
+ gitlab_main_detached_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_20220101"
+ it_behaves_like 'unlock partitions', gitlab_main_detached_partition, 'main'
+ it_behaves_like 'unlock partitions', gitlab_main_detached_partition, 'ci'
end
context 'when running in dry_run mode' do
subject { described_class.new(dry_run: true).lock_writes }
- it 'passes dry_run flag to LockManger' do
+ it 'passes dry_run flag to LockWritesManager' do
expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
- table_name: 'users',
+ table_name: 'security_findings',
connection: anything,
database_name: 'ci',
with_retries: true,
logger: anything,
dry_run: true
- ).and_return(lock_writes_manager)
- expect(lock_writes_manager).to receive(:lock_writes)
+ ).and_return(default_lock_writes_manager)
+ expect(default_lock_writes_manager).to receive(:lock_writes)
subject
end
@@ -185,8 +267,9 @@ RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base
end
it 'does not lock any tables if the ci database is shared with main database' do
- expect(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
- expect(lock_writes_manager).not_to receive(:lock_writes)
+ expect(Gitlab::Database::LockWritesManager).to receive(:new)
+ .with(any_args).and_return(default_lock_writes_manager)
+ expect(default_lock_writes_manager).not_to receive(:lock_writes)
subject
end
@@ -220,7 +303,3 @@ RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base
end
end
end
-
-def number_of_triggers(connection)
- connection.select_value("SELECT count(*) FROM information_schema.triggers")
-end
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
index df17d92bb0c..fb433923db5 100644
--- a/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb
+++ b/spec/lib/gitlab/doorkeeper_secret_storing/secret/pbkdf2_sha512_spec.rb
@@ -10,16 +10,6 @@ RSpec.describe Gitlab::DoorkeeperSecretStoring::Secret::Pbkdf2Sha512 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
@@ -36,7 +26,6 @@ RSpec.describe Gitlab::DoorkeeperSecretStoring::Secret::Pbkdf2Sha512 do
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
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index 8ff8de2379a..369d7e994d2 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -116,7 +116,7 @@ RSpec.describe Gitlab::Email::Handler::CreateIssueHandler do
context "when the issue could not be saved" do
before do
allow_any_instance_of(Issue).to receive(:persisted?).and_return(false)
- allow_any_instance_of(Issue).to receive(:ensure_metrics).and_return(nil)
+ allow_any_instance_of(Issue).to receive(:ensure_metrics!).and_return(nil)
end
it "raises an InvalidIssueError" do
diff --git a/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb b/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb
index fe585d47d59..59c488739dc 100644
--- a/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb
+++ b/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb
@@ -1,17 +1,21 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'kramdown'
+require 'html2text'
+require 'fast_spec_helper'
+require 'support/helpers/fixture_helpers'
RSpec.describe Gitlab::Email::HtmlToMarkdownParser, feature_category: :service_desk do
+ include FixtureHelpers
+
subject { described_class.convert(html) }
describe '.convert' do
let(:html) { fixture_file("lib/gitlab/email/basic.html") }
it 'parses html correctly' do
- expect(subject)
- .to eq(
- <<-BODY.strip_heredoc.chomp
+ expect(subject).to eq(
+ <<~BODY.chomp
Hello, World!
This is some e-mail content. Even though it has whitespace and newlines, the e-mail converter will handle it correctly.
*Even* mismatched tags.
diff --git a/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb b/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb
index 3089f955252..4b77b2f7192 100644
--- a/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb
+++ b/spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb
@@ -2,13 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::Email::Message::BuildIosAppGuide do
+RSpec.describe Gitlab::Email::Message::BuildIosAppGuide, :saas do
subject(:message) { described_class.new }
- before do
- allow(Gitlab).to receive(:com?) { true }
- end
-
it 'contains the correct message', :aggregate_failures do
expect(message.subject_line).to eq 'Get set up to build for iOS'
expect(message.title).to eq "Building for iOS? We've got you covered."
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb
index 3c0d83d0f9e..a3c2d1b428e 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb
@@ -27,11 +27,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Helper do
subject(:class_with_helper) { dummy_class_with_helper.new(format) }
- context 'gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?) { true }
- end
-
+ context 'for SaaS', :saas do
context 'format is HTML' do
it 'returns the correct HTML' do
message = "If you no longer wish to receive marketing emails from us, " \
diff --git a/spec/lib/gitlab/endpoint_attributes_spec.rb b/spec/lib/gitlab/endpoint_attributes_spec.rb
index 53f5b302f05..a623070c3eb 100644
--- a/spec/lib/gitlab/endpoint_attributes_spec.rb
+++ b/spec/lib/gitlab/endpoint_attributes_spec.rb
@@ -1,11 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require_relative '../../support/matchers/be_request_urgency'
-require_relative '../../../lib/gitlab/endpoint_attributes/config'
-require_relative '../../../lib/gitlab/endpoint_attributes'
+require 'spec_helper'
-RSpec.describe Gitlab::EndpointAttributes do
+RSpec.describe Gitlab::EndpointAttributes, feature_category: :api do
let(:base_controller) do
Class.new do
include ::Gitlab::EndpointAttributes
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index fa0b3d1c6dd..d25511843ff 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -145,8 +145,11 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
expect(payload[:headers].env['HTTP_IF_NONE_MATCH']).to eq('W/"123"')
end
- it 'log subscriber processes action' do
- expect_any_instance_of(ActionController::LogSubscriber).to receive(:process_action)
+ it "publishes process_action.action_controller event to be picked up by lograge's subscriber" do
+ # Lograge unhooks the default Rails subscriber (ActionController::LogSubscriber)
+ # and replaces with its own (Lograge::LogSubscribers::ActionController).
+ # When `lograge.keep_original_rails_log = true`, ActionController::LogSubscriber is kept.
+ expect_any_instance_of(Lograge::LogSubscribers::ActionController).to receive(:process_action)
.with(instance_of(ActiveSupport::Notifications::Event))
.and_call_original
diff --git a/spec/lib/gitlab/exception_log_formatter_spec.rb b/spec/lib/gitlab/exception_log_formatter_spec.rb
index 7dda56f0bf5..82166971603 100644
--- a/spec/lib/gitlab/exception_log_formatter_spec.rb
+++ b/spec/lib/gitlab/exception_log_formatter_spec.rb
@@ -45,6 +45,12 @@ RSpec.describe Gitlab::ExceptionLogFormatter do
allow(exception).to receive(:cause).and_return(ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1'))
end
+ it 'adds the cause_class to payload' do
+ described_class.format!(exception, payload)
+
+ expect(payload['exception.cause_class']).to eq('ActiveRecord::StatementInvalid')
+ end
+
it 'adds the normalized SQL query to payload' do
described_class.format!(exception, payload)
diff --git a/spec/lib/gitlab/external_authorization/config_spec.rb b/spec/lib/gitlab/external_authorization/config_spec.rb
index 4231b0d3747..f1daa9249f4 100644
--- a/spec/lib/gitlab/external_authorization/config_spec.rb
+++ b/spec/lib/gitlab/external_authorization/config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ExternalAuthorization::Config, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::ExternalAuthorization::Config, feature_category: :system_access do
it 'allows deploy tokens and keys when external authorization is disabled' do
stub_application_setting(external_authorization_service_enabled: false)
expect(described_class.allow_deploy_tokens_and_deploy_keys?).to be_eql(true)
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 27750f10e87..8afaec3c381 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -13,124 +13,58 @@ RSpec.describe Gitlab::FileFinder, feature_category: :global_search do
let(:expected_file_by_content) { 'CHANGELOG' }
end
- context 'when code_basic_search_files_by_regexp is enabled' do
- before do
- stub_feature_flags(code_basic_search_files_by_regexp: true)
- end
-
- context 'with inclusive filters' do
- it 'filters by filename' do
- results = subject.find('files filename:wm.svg')
-
- expect(results.count).to eq(1)
- end
-
- it 'filters by path' do
- results = subject.find('white path:images')
-
- expect(results.count).to eq(2)
- end
-
- it 'filters by extension' do
- results = subject.find('files extension:md')
-
- expect(results.count).to eq(4)
- end
- end
-
- context 'with exclusive filters' do
- it 'filters by filename' do
- results = subject.find('files -filename:wm.svg')
-
- expect(results.count).to eq(26)
- end
-
- it 'filters by path' do
- results = subject.find('white -path:images')
-
- expect(results.count).to eq(5)
- end
-
- it 'filters by extension' do
- results = subject.find('files -extension:md')
+ context 'with inclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files filename:wm.svg')
- expect(results.count).to eq(23)
- end
+ expect(results.count).to eq(1)
end
- context 'with white space in the path' do
- it 'filters by path correctly' do
- results = subject.find('directory path:"with space/README.md"')
+ it 'filters by path' do
+ results = subject.find('white path:images')
- expect(results.count).to eq(1)
- end
+ expect(results.count).to eq(2)
end
- it 'does not cause N+1 query' do
- expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
+ it 'filters by extension' do
+ results = subject.find('files extension:md')
- subject.find(': filename:wm.svg')
+ expect(results.count).to eq(4)
end
end
- context 'when code_basic_search_files_by_regexp is disabled' do
- before do
- stub_feature_flags(code_basic_search_files_by_regexp: false)
- end
-
- context 'with inclusive filters' do
- it 'filters by filename' do
- results = subject.find('files filename:wm.svg')
-
- expect(results.count).to eq(1)
- end
-
- it 'filters by path' do
- results = subject.find('white path:images')
-
- expect(results.count).to eq(1)
- end
-
- it 'filters by extension' do
- results = subject.find('files extension:md')
+ context 'with exclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files -filename:wm.svg')
- expect(results.count).to eq(4)
- end
+ expect(results.count).to eq(26)
end
- context 'with exclusive filters' do
- it 'filters by filename' do
- results = subject.find('files -filename:wm.svg')
+ it 'filters by path' do
+ results = subject.find('white -path:images')
- expect(results.count).to eq(26)
- end
-
- it 'filters by path' do
- results = subject.find('white -path:images')
-
- expect(results.count).to eq(4)
- end
+ expect(results.count).to eq(5)
+ end
- it 'filters by extension' do
- results = subject.find('files -extension:md')
+ it 'filters by extension' do
+ results = subject.find('files -extension:md')
- expect(results.count).to eq(23)
- end
+ expect(results.count).to eq(23)
end
+ end
- context 'with white space in the path' do
- it 'filters by path correctly' do
- results = subject.find('directory path:"with space/README.md"')
+ context 'with white space in the path' do
+ it 'filters by path correctly' do
+ results = subject.find('directory path:"with space/README.md"')
- expect(results.count).to eq(1)
- end
+ expect(results.count).to eq(1)
end
+ end
- it 'does not cause N+1 query' do
- expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
+ it 'does not cause N+1 query' do
+ expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
- subject.find(': filename:wm.svg')
- end
+ subject.find(': filename:wm.svg')
end
end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index d873151421d..26af9d5d5b8 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -660,7 +660,8 @@ RSpec.describe Gitlab::Git::Commit do
id: SeedRepo::Commit::ID,
message: "tree css fixes",
parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"],
- trailers: {}
+ trailers: {},
+ referenced_by: []
}
end
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 7fa5bd8a92b..5fa0447091c 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -777,6 +777,26 @@ RSpec.describe Gitlab::Git::DiffCollection do
end
end
+ describe '.limits' do
+ let(:options) { {} }
+
+ subject { described_class.limits(options) }
+
+ context 'when options do not include max_patch_bytes_for_file_extension' do
+ it 'sets max_patch_bytes_for_file_extension as empty' do
+ expect(subject[:max_patch_bytes_for_file_extension]).to eq({})
+ end
+ end
+
+ context 'when options include max_patch_bytes_for_file_extension' do
+ let(:options) { { max_patch_bytes_for_file_extension: { '.file' => 1 } } }
+
+ it 'sets value for max_patch_bytes_for_file_extension' do
+ expect(subject[:max_patch_bytes_for_file_extension]).to eq({ '.file' => 1 })
+ end
+ end
+ end
+
def fake_diff(line_length, line_count)
{ 'diff' => "#{'a' * line_length}\n" * line_count }
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 72043ba2a21..483140052f0 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1794,47 +1794,37 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
describe '#license' do
- where(from_gitaly: [true, false])
- with_them do
- subject(:license) { repository.license(from_gitaly) }
-
- context 'when no license file can be found' do
- let_it_be(:project) { create(:project, :repository) }
- let(:repository) { project.repository.raw_repository }
+ subject(:license) { repository.license }
- before do
- project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
- end
+ context 'when no license file can be found' do
+ let_it_be(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw_repository }
- it { is_expected.to be_nil }
+ before do
+ project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
- context 'when an mit license is found' do
- it { is_expected.to have_attributes(key: 'mit') }
- end
+ it { is_expected.to be_nil }
+ end
- context 'when license is not recognized ' do
- let_it_be(:project) { create(:project, :repository) }
- let(:repository) { project.repository.raw_repository }
+ context 'when an mit license is found' do
+ it { is_expected.to have_attributes(key: 'mit') }
+ end
- before do
- project.repository.update_file(
- project.owner,
- 'LICENSE',
- 'This software is licensed under the Dummy license.',
- message: 'Update license',
- branch_name: 'master')
- end
+ context 'when license is not recognized ' do
+ let_it_be(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw_repository }
- it { is_expected.to have_attributes(key: 'other', nickname: 'LICENSE') }
+ before do
+ project.repository.update_file(
+ project.owner,
+ 'LICENSE',
+ 'This software is licensed under the Dummy license.',
+ message: 'Update license',
+ branch_name: 'master')
end
- end
-
- it 'does not crash when license is invalid' do
- expect(Licensee::License).to receive(:new)
- .and_raise(Licensee::InvalidLicense)
- expect(repository.license(false)).to be_nil
+ it { is_expected.to have_attributes(key: 'other', nickname: 'LICENSE') }
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index ea2c239df07..13e9aeb4c53 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitAccess, :aggregate_failures, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::GitAccess, :aggregate_failures, feature_category: :system_access do
include TermsHelper
include AdminModeHelper
include ExternalAuthorizationServiceHelpers
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 09d8ea3cc0a..7bdfa8922d3 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -213,8 +213,13 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
client.local_branches(sort_by: 'name_asc')
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)
+ it 'uses default sort by name' 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: 'invalid')
end
end
@@ -270,6 +275,17 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
client.tags(sort_by: 'version_asc')
end
end
+
+ context 'when sorting option is invalid' do
+ it 'uses default sort by name' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_all_tags)
+ .with(gitaly_request_with_params(sort_by: nil), kind_of(Hash))
+ .and_return([])
+
+ client.tags(sort_by: 'invalid')
+ end
+ end
end
context 'with pagination option' do
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 434550186c1..f457ba06074 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -275,7 +275,8 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
it 'sends a create_repository message without arguments' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:create_repository)
- .with(gitaly_request_with_path(storage_name, relative_path).and(gitaly_request_with_params(default_branch: '')), kind_of(Hash))
+ .with(gitaly_request_with_path(storage_name, relative_path)
+ .and(gitaly_request_with_params(default_branch: '')), kind_of(Hash))
.and_return(double)
client.create_repository
@@ -284,11 +285,23 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
it 'sends a create_repository message with default branch' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:create_repository)
- .with(gitaly_request_with_path(storage_name, relative_path).and(gitaly_request_with_params(default_branch: 'default-branch-name')), kind_of(Hash))
+ .with(gitaly_request_with_path(storage_name, relative_path)
+ .and(gitaly_request_with_params(default_branch: 'default-branch-name')), kind_of(Hash))
.and_return(double)
client.create_repository('default-branch-name')
end
+
+ it 'sends a create_repository message with default branch containing non ascii chars' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_repository)
+ .with(gitaly_request_with_path(storage_name, relative_path)
+ .and(gitaly_request_with_params(
+ default_branch: Gitlab::EncodingHelper.encode_binary('feature/新機能'))), kind_of(Hash)
+ ).and_return(double)
+
+ client.create_repository('feature/新機能')
+ end
end
describe '#create_from_snapshot' do
@@ -314,17 +327,31 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
describe '#search_files_by_regexp' do
- subject(:result) { client.search_files_by_regexp('master', '.*') }
+ subject(:result) { client.search_files_by_regexp(ref, '.*') }
before do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:search_files_by_name)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return([double(files: ['file1.txt']), double(files: ['file2.txt'])])
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([double(files: ['file1.txt']), double(files: ['file2.txt'])])
end
- it 'sends a search_files_by_name message and returns a flatten array' do
- expect(result).to contain_exactly('file1.txt', 'file2.txt')
+ shared_examples 'a search for files by regexp' do
+ it 'sends a search_files_by_name message and returns a flatten array' do
+ expect(result).to contain_exactly('file1.txt', 'file2.txt')
+ end
+ end
+
+ context 'with ASCII ref' do
+ let(:ref) { 'master' }
+
+ it_behaves_like 'a search for files by regexp'
+ end
+
+ context 'with non-ASCII ref' do
+ let(:ref) { 'ref-ñéüçæøß-val' }
+
+ it_behaves_like 'a search for files by regexp'
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index e93d585bc3c..e37b27aeaef 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -600,7 +600,8 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
endCursor
hasNextPage
hasPreviousPage
- }
+ },
+ repositoryCount
}
}
TEXT
@@ -707,44 +708,30 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
end
end
- describe '#search_repos_by_name' do
- let(:expected_query) { 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2' }
-
- it 'searches for repositories based on name' do
- expect(client.octokit).to receive(:search_repositories).with(expected_query, {})
+ describe '#count_repos_by_relation_type_graphql' do
+ relation_types = {
+ 'owned' => ' in:name is:public,private user:user',
+ 'collaborated' => ' in:name is:public,private repo:repo1 repo:repo2',
+ 'organization' => 'org:org1 org:org2'
+ }
- client.search_repos_by_name('test')
- end
+ relation_types.each do |relation_type, expected_query|
+ expected_graphql_params = "type: REPOSITORY, query: \"#{expected_query}\""
+ expected_graphql =
+ <<-TEXT
+ {
+ search(#{expected_graphql_params}) {
+ repositoryCount
+ }
+ }
+ TEXT
- context 'when pagination options present' do
- it 'searches for repositories via expected query' do
- expect(client.octokit).to receive(:search_repositories).with(
- expected_query, { page: 2, per_page: 25 }
+ it 'returns count by relation_type' do
+ expect(client.octokit).to receive(:post).with(
+ '/graphql', { query: expected_graphql }.to_json
)
- client.search_repos_by_name('test', { page: 2, per_page: 25 })
- end
- end
-
- context 'when Faraday error received from octokit', :aggregate_failures do
- let(:error_class) { described_class::CLIENT_CONNECTION_ERROR }
- let(:info_params) { { 'error.class': error_class } }
-
- it 'retries on error and succeeds' do
- allow_retry(:search_repositories)
-
- expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once
-
- expect(client.search_repos_by_name('test')).to eq({})
- end
-
- it 'retries and does not succeed' do
- allow(client.octokit)
- .to receive(:search_repositories)
- .with(expected_query, {})
- .and_raise(error_class, 'execution expired')
-
- expect { client.search_repos_by_name('test') }.to raise_error(error_class, 'execution expired')
+ client.count_repos_by_relation_type_graphql(relation_type: relation_type)
end
end
end
diff --git a/spec/lib/gitlab/github_import/clients/proxy_spec.rb b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
index 0baff7bafcb..7b2a8fa9d74 100644
--- a/spec/lib/gitlab/github_import/clients/proxy_spec.rb
+++ b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
@@ -8,6 +8,10 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category:
let(:access_token) { 'test_token' }
let(:client_options) { { foo: :bar } }
+ it { expect(client).to delegate_method(:each_object).to(:client) }
+ it { expect(client).to delegate_method(:user).to(:client) }
+ it { expect(client).to delegate_method(:octokit).to(:client) }
+
describe '#repos' do
let(:search_text) { 'search text' }
let(:pagination_options) { { limit: 10 } }
@@ -15,54 +19,32 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category:
context 'when remove_legacy_github_client FF is enabled' do
let(:client_stub) { instance_double(Gitlab::GithubImport::Client) }
- context 'with github_client_fetch_repos_via_graphql FF enabled' do
- let(:client_response) do
- {
- data: {
- search: {
- nodes: [{ name: 'foo' }, { name: 'bar' }],
- pageInfo: { startCursor: 'foo', endCursor: 'bar' }
- }
+ let(:client_response) do
+ {
+ data: {
+ search: {
+ nodes: [{ name: 'foo' }, { name: 'bar' }],
+ pageInfo: { startCursor: 'foo', endCursor: 'bar' },
+ repositoryCount: 2
}
}
- end
-
- it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do
- expect(Gitlab::GithubImport::Client)
- .to receive(:new).with(access_token).and_return(client_stub)
- expect(client_stub)
- .to receive(:search_repos_by_name_graphql)
- .with(search_text, pagination_options).and_return(client_response)
-
- expect(client.repos(search_text, pagination_options)).to eq(
- {
- repos: [{ name: 'foo' }, { name: 'bar' }],
- page_info: { startCursor: 'foo', endCursor: 'bar' }
- }
- )
- end
+ }
end
- context 'with github_client_fetch_repos_via_graphql FF disabled' do
- let(:client_response) do
- { items: [{ name: 'foo' }, { name: 'bar' }] }
- end
-
- before do
- stub_feature_flags(github_client_fetch_repos_via_graphql: false)
- end
-
- it 'fetches repos with Gitlab::GithubImport::Client (REST API)' do
- expect(Gitlab::GithubImport::Client)
- .to receive(:new).with(access_token).and_return(client_stub)
- expect(client_stub)
- .to receive(:search_repos_by_name)
- .with(search_text, pagination_options).and_return(client_response)
+ it 'fetches repos with Gitlab::GithubImport::Client (GraphQL API)' do
+ expect(Gitlab::GithubImport::Client)
+ .to receive(:new).with(access_token).and_return(client_stub)
+ expect(client_stub)
+ .to receive(:search_repos_by_name_graphql)
+ .with(search_text, pagination_options).and_return(client_response)
- expect(client.repos(search_text, pagination_options)).to eq(
- { repos: [{ name: 'foo' }, { name: 'bar' }] }
- )
- end
+ expect(client.repos(search_text, pagination_options)).to eq(
+ {
+ repos: [{ name: 'foo' }, { name: 'bar' }],
+ page_info: { startCursor: 'foo', endCursor: 'bar' },
+ count: 2
+ }
+ )
end
end
@@ -99,4 +81,59 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category:
end
end
end
+
+ describe '#count_by', :clean_gitlab_redis_cache do
+ context 'when remove_legacy_github_client FF is enabled' do
+ let(:client_stub) { instance_double(Gitlab::GithubImport::Client) }
+ let(:client_response) { { data: { search: { repositoryCount: 1 } } } }
+
+ before do
+ stub_feature_flags(remove_legacy_github_client: true)
+ end
+
+ context 'when value is cached' do
+ before do
+ Gitlab::Cache::Import::Caching.write('github-importer/provider-repo-count/owned/user_id', 3)
+ end
+
+ it 'returns repository count from cache' do
+ expect(Gitlab::GithubImport::Client)
+ .to receive(:new).with(access_token).and_return(client_stub)
+ expect(client_stub)
+ .not_to receive(:count_repos_by_relation_type_graphql)
+ .with({ relation_type: 'owned' })
+ expect(client.count_repos_by('owned', 'user_id')).to eq(3)
+ end
+ end
+
+ context 'when value is not cached' do
+ it 'returns repository count' do
+ expect(Gitlab::GithubImport::Client)
+ .to receive(:new).with(access_token).and_return(client_stub)
+ expect(client_stub)
+ .to receive(:count_repos_by_relation_type_graphql)
+ .with({ relation_type: 'owned' }).and_return(client_response)
+ expect(Gitlab::Cache::Import::Caching)
+ .to receive(:write)
+ .with('github-importer/provider-repo-count/owned/user_id', 1, timeout: 5.minutes)
+ .and_call_original
+ expect(client.count_repos_by('owned', 'user_id')).to eq(1)
+ end
+ end
+ end
+
+ context 'when remove_legacy_github_client FF is disabled' do
+ let(:client_stub) { instance_double(Gitlab::LegacyGithubImport::Client) }
+
+ before do
+ stub_feature_flags(remove_legacy_github_client: false)
+ end
+
+ it 'returns nil' do
+ expect(Gitlab::LegacyGithubImport::Client)
+ .to receive(:new).with(access_token, client_options).and_return(client_stub)
+ expect(client.count_repos_by('owned', 'user_id')).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/importer/collaborator_importer_spec.rb b/spec/lib/gitlab/github_import/importer/collaborator_importer_spec.rb
new file mode 100644
index 00000000000..07c10fe57f0
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/collaborator_importer_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::CollaboratorImporter, feature_category: :importers do
+ subject(:importer) { described_class.new(collaborator, project, client) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:client) { instance_double(Gitlab::GithubImport::Client) }
+ let(:github_user_id) { rand(1000) }
+ let(:collaborator) do
+ Gitlab::GithubImport::Representation::Collaborator.from_json_hash(
+ 'id' => github_user_id,
+ 'login' => user.username,
+ 'role_name' => github_role_name
+ )
+ end
+
+ let(:basic_member_attrs) do
+ {
+ source: project,
+ user_id: user.id,
+ member_namespace_id: project.project_namespace_id,
+ created_by_id: project.creator_id
+ }.stringify_keys
+ end
+
+ describe '#execute' do
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(github_user_id, user.username).and_return(user.id)
+ end
+ end
+
+ shared_examples 'role mapping' do |collaborator_role, member_access_level|
+ let(:github_role_name) { collaborator_role }
+
+ it 'creates expected member' do
+ expect { importer.execute }.to change { project.members.count }
+ .from(0).to(1)
+
+ expected_member_attrs = basic_member_attrs.merge(access_level: member_access_level)
+ expect(project.members.last).to have_attributes(expected_member_attrs)
+ end
+ end
+
+ it_behaves_like 'role mapping', 'read', Gitlab::Access::GUEST
+ it_behaves_like 'role mapping', 'triage', Gitlab::Access::REPORTER
+ it_behaves_like 'role mapping', 'write', Gitlab::Access::DEVELOPER
+ it_behaves_like 'role mapping', 'maintain', Gitlab::Access::MAINTAINER
+ it_behaves_like 'role mapping', 'admin', Gitlab::Access::OWNER
+
+ context 'when role name is unknown (custom role)' do
+ let(:github_role_name) { 'custom_role' }
+
+ it 'raises expected error' do
+ expect { importer.execute }.to raise_exception(
+ ::Gitlab::GithubImport::ObjectImporter::NotRetriableError
+ ).with_message("Unknown GitHub role: #{github_role_name}")
+ end
+ end
+
+ context 'when user has lower role in a project group' do
+ before do
+ create(:group_member, group: group, user: user, access_level: Gitlab::Access::DEVELOPER)
+ end
+
+ it_behaves_like 'role mapping', 'maintain', Gitlab::Access::MAINTAINER
+ end
+
+ context 'when user has higher role in a project group' do
+ let(:github_role_name) { 'write' }
+
+ before do
+ create(:group_member, group: group, user: user, access_level: Gitlab::Access::MAINTAINER)
+ end
+
+ it 'skips creating member for the project' do
+ expect { importer.execute }.not_to change { project.members.count }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb b/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
new file mode 100644
index 00000000000..e48b562279e
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/collaborators_importer_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::CollaboratorsImporter, feature_category: :importers do
+ subject(:importer) { described_class.new(project, client, parallel: parallel) }
+
+ let(:parallel) { true }
+ let(:project) { instance_double(Project, id: 4, import_source: 'foo/bar', import_state: nil) }
+ let(:client) { instance_double(Gitlab::GithubImport::Client) }
+
+ let(:github_collaborator) do
+ {
+ id: 100500,
+ login: 'bob',
+ role_name: 'maintainer'
+ }
+ end
+
+ describe '#parallel?' do
+ context 'when parallel option is true' do
+ it { expect(importer).to be_parallel }
+ end
+
+ context 'when parallel option is false' 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 collaborators 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 collaborators in sequence' do
+ expect(importer).to receive(:sequential_import)
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ let(:parallel) { false }
+
+ it 'imports each collaborator in sequence' do
+ collaborator_importer = instance_double(Gitlab::GithubImport::Importer::CollaboratorImporter)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_collaborator)
+
+ expect(Gitlab::GithubImport::Importer::CollaboratorImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Collaborator),
+ project,
+ client
+ )
+ .and_return(collaborator_importer)
+
+ expect(collaborator_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import', :clean_gitlab_redis_cache do
+ let(:page_struct) { Struct.new(:objects, :number, keyword_init: true) }
+
+ before do
+ allow(client).to receive(:each_page)
+ .with(:collaborators, project.import_source, { page: 1 })
+ .and_yield(page_struct.new(number: 1, objects: [github_collaborator]))
+ end
+
+ it 'imports each collaborator in parallel' do
+ expect(Gitlab::GithubImport::ImportCollaboratorWorker).to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+
+ context 'when collaborator is already imported' do
+ before do
+ Gitlab::Cache::Import::Caching.set_add(
+ "github-importer/already-imported/#{project.id}/collaborators",
+ github_collaborator[:id]
+ )
+ end
+
+ it "doesn't run importer on it" do
+ expect(Gitlab::GithubImport::ImportCollaboratorWorker).not_to receive(:perform_in)
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(0)
+ end
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the ID of the given note' do
+ expect(importer.id_for_already_imported_cache(github_collaborator))
+ .to eq(100500)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
index e005d8eda84..16816dfbcea 100644
--- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -44,6 +44,10 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
end
it 'does not insert label links for non-existing labels' do
+ expect(importer)
+ .to receive(:find_target_id)
+ .and_return(4)
+
expect(importer.label_finder)
.to receive(:id_for)
.with('bug')
@@ -55,6 +59,20 @@ RSpec.describe Gitlab::GithubImport::Importer::LabelLinksImporter do
importer.create_labels
end
+
+ it 'does not insert label links for non-existing targets' do
+ expect(importer)
+ .to receive(:find_target_id)
+ .and_return(nil)
+
+ expect(importer.label_finder)
+ .not_to receive(:id_for)
+
+ expect(LabelLink)
+ .not_to receive(:bulk_insert!)
+
+ importer.create_labels
+ end
end
describe '#find_target_id' do
diff --git a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb
index 7d4e3c3bcce..450ebe9a719 100644
--- a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
+RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter, feature_category: :importers do
subject(:importer) { described_class.new(note_text, project, client) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, import_source: 'nickname/public-test-repo') }
let(:note_text) { Gitlab::GithubImport::Representation::NoteText.from_db_record(record) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
@@ -13,6 +13,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
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(:image_tag_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ea5.jpeg' }
+ let(:project_blob_url) { 'https://github.com/nickname/public-test-repo/blob/main/example.md' }
+ let(:other_project_blob_url) { 'https://github.com/nickname/other-repo/blob/main/README.md' }
let(:text) do
<<-TEXT.split("\n").map(&:strip).join("\n")
Some text...
@@ -20,11 +22,14 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
[special-doc](#{doc_url})
![image.jpeg](#{image_url})
<img width=\"248\" alt=\"tag-image\" src="#{image_tag_url}">
+
+ [link to project blob file](#{project_blob_url})
+ [link to other project blob file](#{other_project_blob_url})
TEXT
end
shared_examples 'updates record description' do
- it do
+ it 'changes attachment links' do
importer.execute
record.reload
@@ -32,6 +37,22 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
expect(record.description).to include('![image.jpeg](/uploads/')
expect(record.description).to include('<img width="248" alt="tag-image" src="/uploads')
end
+
+ it 'changes link to project blob files' do
+ importer.execute
+
+ record.reload
+ expected_blob_link = "[link to project blob file](http://localhost/#{project.full_path}/-/blob/main/example.md)"
+ expect(record.description).not_to include("[link to project blob file](#{project_blob_url})")
+ expect(record.description).to include(expected_blob_link)
+ end
+
+ it "doesn't change links to other projects" do
+ importer.execute
+
+ record.reload
+ expect(record.description).to include("[link to other project blob file](#{other_project_blob_url})")
+ end
end
describe '#execute' do
@@ -72,7 +93,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
context 'when importing note attachments' do
let(:record) { create(:note, project: project, note: text) }
- it 'updates note text with new attachment urls' do
+ it 'changes note text with new attachment urls' do
importer.execute
record.reload
@@ -80,6 +101,22 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do
expect(record.note).to include('![image.jpeg](/uploads/')
expect(record.note).to include('<img width="248" alt="tag-image" src="/uploads')
end
+
+ it 'changes note links to project blob files' do
+ importer.execute
+
+ record.reload
+ expected_blob_link = "[link to project blob file](http://localhost/#{project.full_path}/-/blob/main/example.md)"
+ expect(record.note).not_to include("[link to project blob file](#{project_blob_url})")
+ expect(record.note).to include(expected_blob_link)
+ end
+
+ it "doesn't change note links to other projects" do
+ importer.execute
+
+ record.reload
+ expect(record.note).to include("[link to other project blob file](#{other_project_blob_url})")
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/markdown/attachment_spec.rb b/spec/lib/gitlab/github_import/markdown/attachment_spec.rb
index 588a3076f59..84b0886ebcc 100644
--- a/spec/lib/gitlab/github_import/markdown/attachment_spec.rb
+++ b/spec/lib/gitlab/github_import/markdown/attachment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Markdown::Attachment do
+RSpec.describe Gitlab::GithubImport::Markdown::Attachment, feature_category: :importers do
let(:name) { FFaker::Lorem.word }
let(:url) { FFaker::Internet.uri('https') }
@@ -101,6 +101,62 @@ RSpec.describe Gitlab::GithubImport::Markdown::Attachment do
end
end
+ describe '#part_of_project_blob?' do
+ let(:attachment) { described_class.new('test', url) }
+ let(:import_source) { 'nickname/public-test-repo' }
+
+ context 'when url is a part of project blob' do
+ let(:url) { "https://github.com/#{import_source}/blob/main/example.md" }
+
+ it { expect(attachment.part_of_project_blob?(import_source)).to eq true }
+ end
+
+ context 'when url is not a part of project blob' do
+ let(:url) { "https://github.com/#{import_source}/files/9020437/git-cheat-sheet.txt" }
+
+ it { expect(attachment.part_of_project_blob?(import_source)).to eq false }
+ end
+ end
+
+ describe '#doc_belongs_to_project?' do
+ let(:attachment) { described_class.new('test', url) }
+ let(:import_source) { 'nickname/public-test-repo' }
+
+ context 'when url relates to this project' do
+ let(:url) { "https://github.com/#{import_source}/files/9020437/git-cheat-sheet.txt" }
+
+ it { expect(attachment.doc_belongs_to_project?(import_source)).to eq true }
+ end
+
+ context 'when url is not related to this project' do
+ let(:url) { 'https://github.com/nickname/other-repo/files/9020437/git-cheat-sheet.txt' }
+
+ it { expect(attachment.doc_belongs_to_project?(import_source)).to eq false }
+ end
+
+ context 'when url is a part of project blob' do
+ let(:url) { "https://github.com/#{import_source}/blob/main/example.md" }
+
+ it { expect(attachment.doc_belongs_to_project?(import_source)).to eq false }
+ end
+ end
+
+ describe '#media?' do
+ let(:attachment) { described_class.new('test', url) }
+
+ context 'when it is a media link' do
+ let(:url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' }
+
+ it { expect(attachment.media?).to eq true }
+ end
+
+ context 'when it is not a media link' do
+ let(:url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' }
+
+ it { expect(attachment.media?).to eq false }
+ end
+ end
+
describe '#inspect' do
it 'returns attachment basic info' do
attachment = described_class.new(name, url)
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index c351ead91eb..9de39a3ff7e 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -289,77 +289,52 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling, feature_category: :impo
.and_return({ title: 'One' }, { title: 'Two' }, { title: 'Three' })
end
- context 'with multiple objects' do
- before do
- stub_feature_flags(improved_spread_parallel_import: false)
-
- expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
- end
-
- it 'imports data in parallel batches with delays' do
- expect(worker_class).to receive(:bulk_perform_in)
- .with(1.second, [
- [project.id, { title: 'One' }, an_instance_of(String)],
- [project.id, { title: 'Two' }, an_instance_of(String)],
- [project.id, { title: 'Three' }, an_instance_of(String)]
- ], batch_size: batch_size, batch_delay: batch_delay)
-
- importer.parallel_import
- end
+ it 'imports data in parallel with delays respecting parallel_import_batch definition and return job waiter' do
+ allow(::Gitlab::JobWaiter).to receive(:generate_key).and_return('waiter-key')
+ allow(importer).to receive(:parallel_import_batch).and_return({ size: 2, delay: 1.minute })
+
+ expect(importer).to receive(:each_object_to_import)
+ .and_yield(object).and_yield(object).and_yield(object)
+ expect(worker_class).to receive(:perform_in)
+ .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ expect(worker_class).to receive(:perform_in)
+ .with(1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ expect(worker_class).to receive(:perform_in)
+ .with(1.minute + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+
+ job_waiter = importer.parallel_import
+
+ expect(job_waiter.key).to eq('waiter-key')
+ expect(job_waiter.jobs_remaining).to eq(3)
end
- context 'when the feature flag `improved_spread_parallel_import` is enabled' do
+ context 'when job restarts due to API rate limit or Sidekiq interruption' do
before do
- stub_feature_flags(improved_spread_parallel_import: true)
+ cache_key = format(described_class::JOB_WAITER_CACHE_KEY,
+ project: project.id, collection: importer.collection_method)
+ Gitlab::Cache::Import::Caching.write(cache_key, 'waiter-key')
+
+ cache_key = format(described_class::JOB_WAITER_REMAINING_CACHE_KEY,
+ project: project.id, collection: importer.collection_method)
+ Gitlab::Cache::Import::Caching.write(cache_key, 3)
end
- it 'imports data in parallel with delays respecting parallel_import_batch definition and return job waiter' do
- allow(::Gitlab::JobWaiter).to receive(:generate_key).and_return('waiter-key')
- allow(importer).to receive(:parallel_import_batch).and_return({ size: 2, delay: 1.minute })
+ it "restores job waiter's key and jobs_remaining" do
+ allow(importer).to receive(:parallel_import_batch).and_return({ size: 1, delay: 1.minute })
+
+ expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
- expect(importer).to receive(:each_object_to_import)
- .and_yield(object).and_yield(object).and_yield(object)
expect(worker_class).to receive(:perform_in)
.with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ .with(1.minute + 1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
expect(worker_class).to receive(:perform_in)
- .with(1.minute + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+ .with(2.minutes + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
job_waiter = importer.parallel_import
expect(job_waiter.key).to eq('waiter-key')
- expect(job_waiter.jobs_remaining).to eq(3)
- end
-
- context 'when job restarts due to API rate limit or Sidekiq interruption' do
- before do
- cache_key = format(described_class::JOB_WAITER_CACHE_KEY,
- project: project.id, collection: importer.collection_method)
- Gitlab::Cache::Import::Caching.write(cache_key, 'waiter-key')
-
- cache_key = format(described_class::JOB_WAITER_REMAINING_CACHE_KEY,
- project: project.id, collection: importer.collection_method)
- Gitlab::Cache::Import::Caching.write(cache_key, 3)
- end
-
- it "restores job waiter's key and jobs_remaining" do
- allow(importer).to receive(:parallel_import_batch).and_return({ size: 1, delay: 1.minute })
-
- expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
-
- expect(worker_class).to receive(:perform_in)
- .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
- expect(worker_class).to receive(:perform_in)
- .with(1.minute + 1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
- expect(worker_class).to receive(:perform_in)
- .with(2.minutes + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
-
- job_waiter = importer.parallel_import
-
- expect(job_waiter.key).to eq('waiter-key')
- expect(job_waiter.jobs_remaining).to eq(6)
- end
+ expect(job_waiter.jobs_remaining).to eq(6)
end
end
end
diff --git a/spec/lib/gitlab/github_import/project_relation_type_spec.rb b/spec/lib/gitlab/github_import/project_relation_type_spec.rb
new file mode 100644
index 00000000000..419cb6de121
--- /dev/null
+++ b/spec/lib/gitlab/github_import/project_relation_type_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::ProjectRelationType, :manage, feature_category: :importers do
+ subject(:project_relation_type) { described_class.new(client) }
+
+ let(:octokit) { instance_double(Octokit::Client) }
+ let(:client) do
+ instance_double(Gitlab::GithubImport::Clients::Proxy, octokit: octokit, user: { login: 'nickname' })
+ end
+
+ describe '#for', :use_clean_rails_redis_caching do
+ before do
+ allow(client).to receive(:each_object).with(:organizations).and_yield({ login: 'great-org' })
+ allow(octokit).to receive(:access_token).and_return('stub')
+ end
+
+ context "when it's user owned repo" do
+ let(:import_source) { 'nickname/repo_name' }
+
+ it { expect(project_relation_type.for(import_source)).to eq 'owned' }
+ end
+
+ context "when it's organization repo" do
+ let(:import_source) { 'great-org/repo_name' }
+
+ it { expect(project_relation_type.for(import_source)).to eq 'organization' }
+ end
+
+ context "when it's user collaborated repo" do
+ let(:import_source) { 'some-another-namespace/repo_name' }
+
+ it { expect(project_relation_type.for(import_source)).to eq 'collaborated' }
+ end
+
+ context 'with cache' do
+ let(:import_source) { 'some-another-namespace/repo_name' }
+
+ it 'calls client only once during 5 minutes timeframe', :request_store do
+ expect(project_relation_type.for(import_source)).to eq 'collaborated'
+ expect(project_relation_type.for('another/repo')).to eq 'collaborated'
+
+ expect(client).to have_received(:each_object).once
+ expect(client).to have_received(:user).once
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/collaborator_spec.rb b/spec/lib/gitlab/github_import/representation/collaborator_spec.rb
new file mode 100644
index 00000000000..d5952f9459b
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/collaborator_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Representation::Collaborator, feature_category: :importers do
+ shared_examples 'a Collaborator' do
+ it 'returns an instance of Collaborator' do
+ expect(collaborator).to be_an_instance_of(described_class)
+ end
+
+ context 'with Collaborator' do
+ it 'includes the user ID' do
+ expect(collaborator.id).to eq(42)
+ end
+
+ it 'includes the username' do
+ expect(collaborator.login).to eq('alice')
+ end
+
+ it 'includes the role' do
+ expect(collaborator.role_name).to eq('maintainer')
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ it_behaves_like 'a Collaborator' do
+ let(:response) { { id: 42, login: 'alice', role_name: 'maintainer' } }
+ let(:collaborator) { described_class.from_api_response(response) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a Collaborator' do
+ let(:hash) { { 'id' => 42, 'login' => 'alice', role_name: 'maintainer' } }
+ let(:collaborator) { described_class.from_json_hash(hash) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n/pluralization_spec.rb b/spec/lib/gitlab/i18n/pluralization_spec.rb
new file mode 100644
index 00000000000..857562d549c
--- /dev/null
+++ b/spec/lib/gitlab/i18n/pluralization_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+require 'gettext_i18n_rails'
+
+RSpec.describe Gitlab::I18n::Pluralization, feature_category: :internationalization do
+ describe '.call' do
+ subject(:rule) { described_class.call(1) }
+
+ context 'with available locales' do
+ around do |example|
+ Gitlab::I18n.with_locale(locale, &example)
+ end
+
+ where(:locale) do
+ Gitlab::I18n.available_locales
+ end
+
+ with_them do
+ it 'supports pluralization' do
+ expect(rule).not_to be_nil
+ end
+ end
+
+ context 'with missing rules' do
+ let(:locale) { "pl_PL" }
+
+ before do
+ stub_const("#{described_class}::MAP", described_class::MAP.except(locale))
+ end
+
+ it 'raises an ArgumentError' do
+ expect { rule }.to raise_error(ArgumentError,
+ /Missing pluralization rule for locale "#{locale}"/
+ )
+ end
+ end
+ end
+ end
+
+ describe '.install_on' do
+ let(:mod) { Module.new }
+
+ before do
+ described_class.install_on(mod)
+ end
+
+ it 'adds pluralisation_rule method' do
+ expect(mod.pluralisation_rule).to eq(described_class)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb
index b752d89bf0d..ee92831922d 100644
--- a/spec/lib/gitlab/i18n_spec.rb
+++ b/spec/lib/gitlab/i18n_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::I18n do
+RSpec.describe Gitlab::I18n, feature_category: :internationalization do
let(:user) { create(:user, preferred_language: :es) }
describe '.selectable_locales' do
@@ -47,4 +47,19 @@ RSpec.describe Gitlab::I18n do
expect(::I18n.locale).to eq(:en)
end
end
+
+ describe '.pluralisation_rule' do
+ context 'when overridden' do
+ before do
+ # Internally, FastGettext sets
+ # Thread.current[:fast_gettext_pluralisation_rule].
+ # Our patch patches `FastGettext.pluralisation_rule` instead.
+ FastGettext.pluralisation_rule = :something
+ end
+
+ it 'returns custom definition regardless' do
+ expect(FastGettext.pluralisation_rule).to eq(Gitlab::I18n::Pluralization)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import/errors_spec.rb b/spec/lib/gitlab/import/errors_spec.rb
new file mode 100644
index 00000000000..f89cb36bbb4
--- /dev/null
+++ b/spec/lib/gitlab/import/errors_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Import::Errors, feature_category: :importers do
+ let_it_be(:project) { create(:project) }
+
+ describe '.merge_nested_errors' do
+ it 'merges nested collection errors' do
+ issue = project.issues.new(
+ title: 'test',
+ notes: [
+ Note.new(
+ award_emoji: [AwardEmoji.new(name: 'test')]
+ )
+ ],
+ sentry_issue: SentryIssue.new
+ )
+
+ issue.validate
+
+ expect(issue.errors.full_messages)
+ .to contain_exactly(
+ "Author can't be blank",
+ "Notes is invalid",
+ "Sentry issue sentry issue identifier can't be blank"
+ )
+
+ described_class.merge_nested_errors(issue)
+
+ expect(issue.errors.full_messages)
+ .to contain_exactly(
+ "Notes is invalid",
+ "Author can't be blank",
+ "Sentry issue sentry issue identifier can't be blank",
+ "Award emoji is invalid",
+ "Note can't be blank",
+ "Project can't be blank",
+ "Noteable can't be blank",
+ "Author can't be blank",
+ "Project does not match noteable project",
+ "User can't be blank",
+ "Awardable can't be blank",
+ "Name is not a valid emoji name"
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import/metrics_spec.rb b/spec/lib/gitlab/import/metrics_spec.rb
index 9b8b58d00f3..1a988af0dbd 100644
--- a/spec/lib/gitlab/import/metrics_spec.rb
+++ b/spec/lib/gitlab/import/metrics_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do
subject { described_class.new(importer, project) }
before do
- allow(Gitlab::Metrics).to receive(:counter) { counter }
allow(counter).to receive(:increment)
allow(histogram).to receive(:observe)
end
@@ -42,6 +41,13 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do
context 'when project is not a github import' do
it 'does not emit importer metrics' do
expect(subject).not_to receive(:track_usage_event)
+ expect_no_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'failed' }
+ )
subject.track_failed_import
end
@@ -50,39 +56,81 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do
context 'when project is a github import' do
before do
project.import_type = 'github'
+ allow(project).to receive(:import_status).and_return('failed')
end
it 'emits importer metrics' do
expect(subject).to receive(:track_usage_event).with(:github_import_project_failure, project.id)
subject.track_failed_import
+
+ expect_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'failed' }
+ )
end
end
end
describe '#track_finished_import' do
- before do
- allow(Gitlab::Metrics).to receive(:histogram) { histogram }
- end
+ context 'when project is a github import' do
+ before do
+ project.import_type = 'github'
+ allow(Gitlab::Metrics).to receive(:counter) { counter }
+ allow(Gitlab::Metrics).to receive(:histogram) { histogram }
+ allow(project).to receive(:beautified_import_status_name).and_return('completed')
+ end
- it 'emits importer metrics' do
- expect(Gitlab::Metrics).to receive(:counter).with(
- :test_importer_imported_projects_total,
- 'The number of imported projects'
- )
+ it 'emits importer metrics' do
+ expect(Gitlab::Metrics).to receive(:counter).with(
+ :test_importer_imported_projects_total,
+ 'The number of imported projects'
+ )
- expect(Gitlab::Metrics).to receive(:histogram).with(
- :test_importer_total_duration_seconds,
- 'Total time spent importing projects, in seconds',
- {},
- described_class::IMPORT_DURATION_BUCKETS
- )
+ expect(Gitlab::Metrics).to receive(:histogram).with(
+ :test_importer_total_duration_seconds,
+ 'Total time spent importing projects, in seconds',
+ {},
+ described_class::IMPORT_DURATION_BUCKETS
+ )
+
+ expect(counter).to receive(:increment)
- expect(counter).to receive(:increment)
+ subject.track_finished_import
- subject.track_finished_import
+ expect_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'completed' }
+ )
+
+ expect(subject.duration).not_to be_nil
+ end
- expect(subject.duration).not_to be_nil
+ context 'when import is partially completed' do
+ before do
+ allow(project).to receive(:beautified_import_status_name).and_return('partially completed')
+ end
+
+ it 'emits snowplow metrics' do
+ expect(subject).to receive(:track_usage_event).with(:github_import_project_partially_completed, project.id)
+
+ subject.track_finished_import
+
+ expect_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'partially completed' }
+ )
+ end
+ end
end
context 'when project is not a github import' do
@@ -91,7 +139,51 @@ RSpec.describe Gitlab::Import::Metrics, :aggregate_failures do
subject.track_finished_import
- expect(histogram).to have_received(:observe).with({ importer: :test_importer }, anything)
+ expect_no_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'completed' }
+ )
+ end
+ end
+ end
+
+ describe '#track_cancelled_import' do
+ context 'when project is not a github import' do
+ it 'does not emit importer metrics' do
+ expect(subject).not_to receive(:track_usage_event)
+ expect_no_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'canceled' }
+ )
+
+ subject.track_canceled_import
+ end
+ end
+
+ context 'when project is a github import' do
+ before do
+ project.import_type = 'github'
+ allow(project).to receive(:import_status).and_return('canceled')
+ end
+
+ it 'emits importer metrics' do
+ expect(subject).to receive(:track_usage_event).with(:github_import_project_cancelled, project.id)
+
+ subject.track_canceled_import
+
+ expect_snowplow_event(
+ category: :test_importer,
+ action: 'create',
+ label: 'github_import_project_state',
+ project: project,
+ extra: { import_type: 'github', state: 'canceled' }
+ )
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 0c2c3ffc664..f6d6a791e8c 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -92,6 +92,20 @@ notes:
- suggestions
- diff_note_positions
- review
+commit_notes:
+- award_emoji
+- noteable
+- author
+- updated_by
+- last_edited_by
+- resolved_by
+- todos
+- events
+- system_note_metadata
+- note_diff_file
+- suggestions
+- diff_note_positions
+- review
label_links:
- target
- label
@@ -283,6 +297,7 @@ ci_pipelines:
- job_artifacts
- vulnerabilities_finding_pipelines
- vulnerability_findings
+- vulnerability_state_transitions
- pipeline_config
- security_scans
- security_findings
@@ -317,6 +332,7 @@ stages:
- processables
- builds
- bridges
+- generic_commit_statuses
- latest_statuses
- retried_statuses
statuses:
@@ -327,6 +343,92 @@ statuses:
- auto_canceled_by
- needs
- ci_stage
+builds:
+- user
+- auto_canceled_by
+- ci_stage
+- needs
+- resource
+- pipeline
+- sourced_pipeline
+- resource_group
+- metadata
+- runner
+- trigger_request
+- erased_by
+- deployment
+- pending_state
+- queuing_entry
+- runtime_metadata
+- trace_chunks
+- report_results
+- namespace
+- job_artifacts
+- job_variables
+- sourced_pipelines
+- pages_deployments
+- job_artifacts_archive
+- job_artifacts_metadata
+- job_artifacts_trace
+- job_artifacts_junit
+- job_artifacts_sast
+- job_artifacts_dependency_scanning
+- job_artifacts_container_scanning
+- job_artifacts_dast
+- job_artifacts_codequality
+- job_artifacts_license_scanning
+- job_artifacts_performance
+- job_artifacts_metrics
+- job_artifacts_metrics_referee
+- job_artifacts_network_referee
+- job_artifacts_lsif
+- job_artifacts_dotenv
+- job_artifacts_cobertura
+- job_artifacts_terraform
+- job_artifacts_accessibility
+- job_artifacts_cluster_applications
+- job_artifacts_secret_detection
+- job_artifacts_requirements
+- job_artifacts_coverage_fuzzing
+- job_artifacts_browser_performance
+- job_artifacts_load_performance
+- job_artifacts_api_fuzzing
+- job_artifacts_cluster_image_scanning
+- job_artifacts_cyclonedx
+- job_artifacts_requirements_v2
+- runner_machine
+- runner_machine_build
+- runner_session
+- trace_metadata
+- terraform_state_versions
+- taggings
+- base_tags
+- tag_taggings
+- tags
+- security_scans
+- dast_site_profiles_build
+- dast_site_profile
+- dast_scanner_profiles_build
+- dast_scanner_profile
+bridges:
+- user
+- pipeline
+- auto_canceled_by
+- ci_stage
+- needs
+- resource
+- sourced_pipeline
+- resource_group
+- metadata
+- trigger_request
+- downstream_pipeline
+- upstream_pipeline
+generic_commit_statuses:
+- user
+- pipeline
+- auto_canceled_by
+- ci_stage
+- needs
variables:
- project
triggers:
@@ -408,6 +510,7 @@ project:
- project_namespace
- management_clusters
- boards
+- application_setting
- last_event
- integrations
- push_hooks_integrations
@@ -432,6 +535,7 @@ project:
- discord_integration
- drone_ci_integration
- emails_on_push_integration
+- google_play_integration
- pipelines_email_integration
- mattermost_slash_commands_integration
- shimo_integration
@@ -466,6 +570,7 @@ project:
- external_wiki_integration
- mock_ci_integration
- mock_monitoring_integration
+- squash_tm_integration
- forked_to_members
- forked_from_project
- forks
@@ -600,7 +705,7 @@ project:
- project_registry
- packages
- package_files
-- repository_files
+- rpm_repository_files
- packages_cleanup_policy
- alerting_setting
- project_setting
@@ -618,6 +723,7 @@ project:
- upstream_project_subscriptions
- downstream_project_subscriptions
- service_desk_setting
+- service_desk_custom_email_verification
- security_setting
- import_failures
- container_expiration_policy
@@ -859,6 +965,8 @@ bulk_import_export:
- group
service_desk_setting:
- file_template_project
+service_desk_custom_email_verification:
+ - triggerer
approvals:
- user
- merge_request
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 572f809e43b..1d84cba3825 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -9,7 +9,7 @@ require 'spec_helper'
# to be included as part of the export, or blacklist them using the import_export.yml configuration file.
# Likewise, new models added to import_export.yml, will need to be added with their correspondent attributes
# to this spec.
-RSpec.describe 'Import/Export attribute configuration' do
+RSpec.describe 'Import/Export attribute configuration', feature_category: :importers do
include ConfigurationHelper
let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
index 6536b895b2f..767b7a3c84e 100644
--- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::ImportExport::AttributesFinder do
+RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :importers do
describe '#find_root' do
subject { described_class.new(config: config).find_root(model_key) }
@@ -207,6 +207,19 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder do
it { is_expected.to be_nil }
end
+
+ context 'when include_import_only_tree is true' do
+ subject { described_class.new(config: config).find_relations_tree(model_key, include_import_only_tree: true) }
+
+ let(:config) do
+ {
+ tree: { project: { ci_pipelines: { stages: { builds: nil } } } },
+ import_only_tree: { project: { ci_pipelines: { stages: { statuses: nil } } } }
+ }
+ end
+
+ it { is_expected.to eq({ ci_pipelines: { stages: { builds: nil, statuses: nil } } }) }
+ end
end
describe '#find_excluded_keys' do
diff --git a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
index c748f966463..8089b40cae8 100644
--- a/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_permitter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::AttributesPermitter do
+RSpec.describe Gitlab::ImportExport::AttributesPermitter, feature_category: :importers do
let(:yml_config) do
<<-EOF
tree:
@@ -12,6 +12,15 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do
- milestones:
- events:
- :push_event_payload
+ - ci_pipelines:
+ - stages:
+ - :builds
+
+ import_only_tree:
+ project:
+ - ci_pipelines:
+ - stages:
+ - :statuses
included_attributes:
labels:
@@ -43,12 +52,16 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do
it 'builds permitted attributes hash' do
expect(subject.permitted_attributes).to match(
a_hash_including(
- project: [:labels, :milestones],
+ project: [:labels, :milestones, :ci_pipelines],
labels: [:priorities, :title, :description, :type],
events: [:push_event_payload],
milestones: [:events],
priorities: [],
- push_event_payload: []
+ push_event_payload: [],
+ ci_pipelines: [:stages],
+ stages: [:builds, :statuses],
+ statuses: [],
+ builds: []
)
)
end
@@ -129,6 +142,9 @@ RSpec.describe Gitlab::ImportExport::AttributesPermitter do
:external_pull_request | true
:external_pull_requests | true
:statuses | true
+ :builds | true
+ :generic_commit_statuses | true
+ :bridges | true
:ci_pipelines | true
:stages | true
:actions | true
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 a8b4b9a6f05..e42a1d0ff8b 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
@@ -82,24 +82,13 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver, feature_category
it 'saves valid subrelations and logs invalid subrelation' do
expect(relation_object.notes).to receive(:<<).twice.and_call_original
expect(relation_object).to receive(:save).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: "Project does not match noteable project"
- )
saver.execute
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('Project does not match noteable project')
end
context 'when invalid subrelation can still be persisted' do
@@ -111,7 +100,6 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver, feature_category
it 'saves the subrelation' do
expect(approval_1.valid?).to eq(false)
- expect(Gitlab::Import::Logger).not_to receive(:info)
saver.execute
@@ -128,24 +116,10 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver, feature_category
let(:invalid_priority) { build(:label_priority, priority: -1) }
let(:relation_object) { build(:group_label, group: importable, title: 'test', priorities: valid_priorities + [invalid_priority]) }
- it 'logs invalid subrelation for a group' do
- expect(Gitlab::Import::Logger)
- .to receive(:info)
- .with(
- message: '[Project/Group Import] Invalid subrelation',
- group_id: importable.id,
- relation_key: 'labels',
- error_messages: 'Priority must be greater than or equal to 0'
- )
-
+ it 'saves relation without invalid subrelations' do
saver.execute
- label = importable.labels.last
- import_failure = importable.import_failures.last
-
- expect(label.priorities.count).to eq(5)
- expect(import_failure.source).to eq('RelationObjectSaver#save!')
- expect(import_failure.exception_message).to eq('Priority must be greater than or equal to 0')
+ expect(importable.labels.last.priorities.count).to eq(5)
end
end
end
diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb
index f47f1ab58a8..91cfab1688a 100644
--- a/spec/lib/gitlab/import_export/command_line_util_spec.rb
+++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb
@@ -2,13 +2,14 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::CommandLineUtil do
+RSpec.describe Gitlab::ImportExport::CommandLineUtil, feature_category: :importers do
include ExportFileHelper
let(:path) { "#{Dir.tmpdir}/symlink_test" }
let(:archive) { 'spec/fixtures/symlink_export.tar.gz' }
let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
let(:tmpdir) { Dir.mktmpdir }
+ let(:archive_dir) { Dir.mktmpdir }
subject do
Class.new do
@@ -25,20 +26,38 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
before do
FileUtils.mkdir_p(path)
- subject.untar_zxf(archive: archive, dir: path)
end
after do
FileUtils.rm_rf(path)
+ FileUtils.rm_rf(archive_dir)
FileUtils.remove_entry(tmpdir)
end
- it 'has the right mask for project.json' do
- expect(file_permissions("#{path}/project.json")).to eq(0755) # originally 777
- end
-
- it 'has the right mask for uploads' do
- expect(file_permissions("#{path}/uploads")).to eq(0755) # originally 555
+ shared_examples 'deletes symlinks' do |compression, decompression|
+ it 'deletes the symlinks', :aggregate_failures do
+ Dir.mkdir("#{tmpdir}/.git")
+ Dir.mkdir("#{tmpdir}/folder")
+ FileUtils.touch("#{tmpdir}/file.txt")
+ FileUtils.touch("#{tmpdir}/folder/file.txt")
+ FileUtils.touch("#{tmpdir}/.gitignore")
+ FileUtils.touch("#{tmpdir}/.git/config")
+ File.symlink('file.txt', "#{tmpdir}/.symlink")
+ File.symlink('file.txt', "#{tmpdir}/.git/.symlink")
+ File.symlink('file.txt', "#{tmpdir}/folder/.symlink")
+ archive = File.join(archive_dir, 'archive')
+ subject.public_send(compression, archive: archive, dir: tmpdir)
+
+ subject.public_send(decompression, archive: archive, dir: archive_dir)
+
+ expect(File.exist?("#{archive_dir}/file.txt")).to eq(true)
+ expect(File.exist?("#{archive_dir}/folder/file.txt")).to eq(true)
+ expect(File.exist?("#{archive_dir}/.gitignore")).to eq(true)
+ expect(File.exist?("#{archive_dir}/.git/config")).to eq(true)
+ expect(File.exist?("#{archive_dir}/.symlink")).to eq(false)
+ expect(File.exist?("#{archive_dir}/.git/.symlink")).to eq(false)
+ expect(File.exist?("#{archive_dir}/folder/.symlink")).to eq(false)
+ end
end
describe '#download_or_copy_upload' do
@@ -228,12 +247,6 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
end
describe '#tar_cf' do
- let(:archive_dir) { Dir.mktmpdir }
-
- after do
- FileUtils.remove_entry(archive_dir)
- end
-
it 'archives a folder without compression' do
archive_file = File.join(archive_dir, 'archive.tar')
@@ -256,12 +269,24 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
end
end
- describe '#untar_xf' do
- let(:archive_dir) { Dir.mktmpdir }
+ describe '#untar_zxf' do
+ it_behaves_like 'deletes symlinks', :tar_czf, :untar_zxf
- after do
- FileUtils.remove_entry(archive_dir)
+ it 'has the right mask for project.json' do
+ subject.untar_zxf(archive: archive, dir: path)
+
+ expect(file_permissions("#{path}/project.json")).to eq(0755) # originally 777
+ end
+
+ it 'has the right mask for uploads' do
+ subject.untar_zxf(archive: archive, dir: path)
+
+ expect(file_permissions("#{path}/uploads")).to eq(0755) # originally 555
end
+ end
+
+ describe '#untar_xf' do
+ it_behaves_like 'deletes symlinks', :tar_cf, :untar_xf
it 'extracts archive without decompression' do
filename = 'archive.tar.gz'
diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb
index 8f848af8bd3..2a52a0a2ff2 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Config do
+RSpec.describe Gitlab::ImportExport::Config, feature_category: :importers do
let(:yaml_file) { described_class.new }
describe '#to_h' do
@@ -21,7 +21,9 @@ 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 = [
+ :tree, :import_only_tree, :excluded_attributes, :included_attributes, :methods, :preloads, :export_reorders
+ ]
expected_keys << :include_if_exportable if ee
expect { subject }.not_to raise_error
@@ -82,7 +84,7 @@ RSpec.describe Gitlab::ImportExport::Config do
EOF
end
- let(:config_hash) { YAML.safe_load(config, [Symbol]) }
+ let(:config_hash) { YAML.safe_load(config, permitted_classes: [Symbol]) }
before do
allow_any_instance_of(described_class).to receive(:parse_yaml) do
@@ -110,6 +112,7 @@ RSpec.describe Gitlab::ImportExport::Config do
}
}
},
+ import_only_tree: {},
included_attributes: {
user: [:id]
},
@@ -153,6 +156,7 @@ RSpec.describe Gitlab::ImportExport::Config do
}
}
},
+ import_only_tree: {},
included_attributes: {
user: [:id, :name_ee]
},
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index f18d9e64f52..02419267f0e 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license do
+RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_category: :importers do
# FastHashSerializer#execute generates the hash which is not easily accessible
# and includes `JSONBatchRelation` items which are serialized at this point.
# Wrapping the result into JSON generating/parsing is for making
@@ -125,13 +125,13 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license do
expect(subject.dig('ci_pipelines', 0, 'stages')).not_to be_empty
end
- it 'has pipeline statuses' do
- expect(subject.dig('ci_pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty
+ it 'has pipeline builds' do
+ expect(subject.dig('ci_pipelines', 0, 'stages', 0, 'builds')).not_to be_empty
end
it 'has pipeline builds' do
builds_count = subject
- .dig('ci_pipelines', 0, 'stages', 0, 'statuses')
+ .dig('ci_pipelines', 0, 'stages', 0, 'builds')
.count { |hash| hash['type'] == 'Ci::Build' }
expect(builds_count).to eq(1)
@@ -141,8 +141,8 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license do
expect(subject['ci_pipelines']).not_to be_empty
end
- it 'has ci pipeline notes' do
- expect(subject['ci_pipelines'].first['notes']).not_to be_empty
+ it 'has commit notes' do
+ expect(subject['commit_notes']).not_to be_empty
end
it 'has labels with no associations' do
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 5e84284a060..07971d6271c 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
@@ -9,7 +9,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
+RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_category: :importers do
let(:group) { create(:group).tap { |g| g.add_owner(user) } }
let(:importable) { create(:group, parent: group) }
@@ -60,4 +60,81 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
subject
end
+
+ describe 'relation object saving' do
+ let(:importable) { create(:group) }
+ let(:relation_reader) do
+ Gitlab::ImportExport::Json::LegacyReader::File.new(
+ path,
+ relation_names: [:labels])
+ end
+
+ before do
+ allow(shared.logger).to receive(:info).and_call_original
+ allow(relation_reader).to receive(:consume_relation).and_call_original
+
+ allow(relation_reader)
+ .to receive(:consume_relation)
+ .with(nil, 'labels')
+ .and_return([[label, 0]])
+ end
+
+ context 'when relation object is new' do
+ context 'when relation object has invalid subrelations' do
+ let(:label) do
+ {
+ 'title' => 'test',
+ 'priorities' => [LabelPriority.new, LabelPriority.new],
+ 'type' => 'GroupLabel'
+ }
+ end
+
+ it 'logs invalid subrelations' do
+ expect(shared.logger)
+ .to receive(:info)
+ .with(
+ message: '[Project/Group Import] Invalid subrelation',
+ group_id: importable.id,
+ relation_key: 'labels',
+ error_messages: "Project can't be blank, Priority can't be blank, and Priority is not a number"
+ )
+
+ subject
+
+ label = importable.labels.first
+ failure = importable.import_failures.first
+
+ expect(importable.import_failures.count).to eq(2)
+ expect(label.title).to eq('test')
+ expect(failure.exception_class).to eq('ActiveRecord::RecordInvalid')
+ expect(failure.source).to eq('RelationTreeRestorer#save_relation_object')
+ expect(failure.exception_message)
+ .to eq("Project can't be blank, Priority can't be blank, and Priority is not a number")
+ end
+ end
+ end
+
+ context 'when relation object is persisted' do
+ context 'when relation object is invalid' do
+ let(:label) { create(:group_label, group: group, title: 'test') }
+
+ it 'saves import failure with nested errors' do
+ label.priorities << [LabelPriority.new, LabelPriority.new]
+
+ subject
+
+ failure = importable.import_failures.first
+
+ expect(importable.labels.count).to eq(0)
+ expect(importable.import_failures.count).to eq(1)
+ expect(failure.exception_class).to eq('ActiveRecord::RecordInvalid')
+ expect(failure.source).to eq('process_relation_item!')
+ expect(failure.exception_message)
+ .to eq("Validation failed: Priorities is invalid, Project can't be blank, Priority can't be blank, " \
+ "Priority is not a number, Project can't be blank, Priority can't be blank, " \
+ "Priority is not a number")
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/import_failure_service_spec.rb b/spec/lib/gitlab/import_export/import_failure_service_spec.rb
index 51f1fc9c6a2..30d16347828 100644
--- a/spec/lib/gitlab/import_export/import_failure_service_spec.rb
+++ b/spec/lib/gitlab/import_export/import_failure_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::ImportFailureService do
+RSpec.describe Gitlab::ImportExport::ImportFailureService, feature_category: :importers do
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
let(:label) { create(:label) }
let(:subject) { described_class.new(importable) }
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 e8ecd98b1e1..2c0f023ad2c 100644
--- a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tmpdir'
-RSpec.describe Gitlab::ImportExport::Json::LegacyWriter do
+RSpec.describe Gitlab::ImportExport::Json::LegacyWriter, feature_category: :importers do
let(:path) { "#{Dir.tmpdir}/legacy_writer_spec/test.json" }
subject do
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 4f01f470ce7..ce965a05a32 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
# Part of the test security suite for the Import/Export feature
# Finds if a new model has been added that can potentially be part of the Import/Export
# If it finds a new model, it will show a +failure_message+ with the options available.
-RSpec.describe 'Import/Export model configuration' do
+RSpec.describe 'Import/Export model configuration', feature_category: :importers do
include ConfigurationHelper
let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
diff --git a/spec/lib/gitlab/import_export/project/import_task_spec.rb b/spec/lib/gitlab/import_export/project/import_task_spec.rb
index c847224cb9b..693f1984ce8 100644
--- a/spec/lib/gitlab/import_export/project/import_task_spec.rb
+++ b/spec/lib/gitlab/import_export/project/import_task_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store, :silence_stdout do
+RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store, :silence_stdout, feature_category: :importers do
let(:username) { 'root' }
let(:namespace_path) { username }
let!(:user) { create(:user, username: username) }
diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
index 189b798c2e8..5fa8590e8fd 100644
--- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb
@@ -86,13 +86,16 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
'group' => group)).to eq(group_label)
end
- it 'creates a new label' do
+ it 'creates a new project label' do
label = described_class.build(Label,
'title' => 'group label',
'project' => project,
- 'group' => project.group)
+ 'group' => project.group,
+ 'group_id' => project.group.id)
expect(label.persisted?).to be true
+ expect(label).to be_an_instance_of(ProjectLabel)
+ expect(label.group_id).to be_nil
end
end
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 6053df8ba97..75012aa80ec 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
@@ -50,6 +50,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer, feature_cate
expect(project.custom_attributes.count).to eq(2)
expect(project.project_badges.count).to eq(2)
expect(project.snippets.count).to eq(1)
+ expect(project.commit_notes.count).to eq(3)
end
end
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 125d1736b9b..a07fe4fd29c 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -295,6 +295,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
it 'has project labels' do
expect(ProjectLabel.count).to eq(3)
+ expect(ProjectLabel.pluck(:group_id).compact).to be_empty
end
it 'has merge request approvals' do
@@ -528,7 +529,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
it 'has the correct number of pipelines and statuses' do
expect(@project.ci_pipelines.size).to eq(7)
- @project.ci_pipelines.order(:id).zip([2, 0, 2, 2, 2, 2, 0])
+ @project.ci_pipelines.order(:id).zip([2, 0, 2, 3, 2, 2, 0])
.each do |(pipeline, expected_status_size)|
expect(pipeline.statuses.size).to eq(expected_status_size)
end
@@ -548,8 +549,16 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
expect(Ci::Stage.all).to all(have_attributes(pipeline_id: a_value > 0))
end
- it 'restores statuses' do
- expect(CommitStatus.all.count).to be 10
+ it 'restores builds' do
+ expect(Ci::Build.all.count).to be 7
+ end
+
+ it 'restores bridges' do
+ expect(Ci::Bridge.all.count).to be 1
+ end
+
+ it 'restores generic commit statuses' do
+ expect(GenericCommitStatus.all.count).to be 1
end
it 'correctly restores association between a stage and a job' do
@@ -574,6 +583,10 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
expect(@project.import_failures.size).to eq 0
end
end
+
+ it 'restores commit notes' do
+ expect(@project.commit_notes.count).to eq(3)
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 74b6e039601..b87992c4594 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license do
+RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_category: :importers do
let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let_it_be(:exportable_path) { 'project' }
let_it_be(:user) { create(:user) }
@@ -223,22 +223,31 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license do
expect(subject.dig(0, 'stages')).not_to be_empty
end
- it 'has pipeline statuses' do
- expect(subject.dig(0, 'stages', 0, 'statuses')).not_to be_empty
+ it 'has pipeline builds' do
+ count = subject.dig(0, 'stages', 0, 'builds').count
+
+ expect(count).to eq(1)
end
- it 'has pipeline builds' do
- builds_count = subject.dig(0, 'stages', 0, 'statuses')
- .count { |hash| hash['type'] == 'Ci::Build' }
+ it 'has pipeline generic_commit_statuses' do
+ count = subject.dig(0, 'stages', 0, 'generic_commit_statuses').count
- expect(builds_count).to eq(1)
+ expect(count).to eq(1)
end
- it 'has ci pipeline notes' do
- expect(subject.first['notes']).not_to be_empty
+ it 'has pipeline bridges' do
+ count = subject.dig(0, 'stages', 0, 'bridges').count
+
+ expect(count).to eq(1)
end
end
+ context 'with commit_notes' do
+ let(:relation_name) { :commit_notes }
+
+ it { is_expected.not_to be_empty }
+ end
+
context 'with labels' do
let(:relation_name) { :labels }
@@ -468,6 +477,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license do
end
end
+ # rubocop: disable Metrics/AbcSize
def setup_project
release = create(:release)
@@ -496,6 +506,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license do
ci_build = create(:ci_build, project: project, when: nil)
ci_build.pipeline.update!(project: project)
create(:commit_status, project: project, pipeline: ci_build.pipeline)
+ create(:generic_commit_status, pipeline: ci_build.pipeline, ci_stage: ci_build.ci_stage, project: project)
+ create(:ci_bridge, pipeline: ci_build.pipeline, ci_stage: ci_build.ci_stage, project: project)
create(:milestone, project: project)
discussion_note = create(:discussion_note, noteable: issue, project: project)
@@ -528,4 +540,5 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license do
project
end
+ # rubocop: enable Metrics/AbcSize
end
diff --git a/spec/lib/gitlab/import_export/references_configuration_spec.rb b/spec/lib/gitlab/import_export/references_configuration_spec.rb
index ad165790b77..84c5b564cb1 100644
--- a/spec/lib/gitlab/import_export/references_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/references_configuration_spec.rb
@@ -9,7 +9,7 @@ require 'spec_helper'
# or to be blacklisted by using the import_export.yml configuration file.
# Likewise, new models added to import_export.yml, will need to be added with their correspondent relations
# to this spec.
-RSpec.describe 'Import/Export Project configuration' do
+RSpec.describe 'Import/Export Project configuration', feature_category: :importers do
include ConfigurationHelper
where(:relation_path, :relation_name) do
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index e14e929faf3..2384baabb6b 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -86,6 +86,7 @@ Note:
- original_discussion_id
- confidential
- last_edited_at
+- internal
LabelLink:
- id
- target_type
@@ -347,7 +348,111 @@ Ci::Stage:
- pipeline_id
- created_at
- updated_at
-CommitStatus:
+Ci::Build:
+- id
+- project_id
+- status
+- finished_at
+- trace
+- created_at
+- updated_at
+- started_at
+- runner_id
+- coverage
+- commit_id
+- commands
+- job_id
+- name
+- deploy
+- options
+- allow_failure
+- stage
+- trigger_request_id
+- stage_idx
+- stage_id
+- tag
+- ref
+- user_id
+- type
+- target_url
+- description
+- artifacts_file
+- artifacts_file_store
+- artifacts_metadata
+- artifacts_metadata_store
+- erased_by_id
+- erased_at
+- artifacts_expire_at
+- environment
+- artifacts_size
+- when
+- yaml_variables
+- queued_at
+- token
+- lock_version
+- coverage_regex
+- auto_canceled_by_id
+- retried
+- protected
+- failure_reason
+- scheduled_at
+- upstream_pipeline_id
+- interruptible
+- processed
+- scheduling_type
+Ci::Bridge:
+- id
+- project_id
+- status
+- finished_at
+- trace
+- created_at
+- updated_at
+- started_at
+- runner_id
+- coverage
+- commit_id
+- commands
+- job_id
+- name
+- deploy
+- options
+- allow_failure
+- stage
+- trigger_request_id
+- stage_idx
+- stage_id
+- tag
+- ref
+- user_id
+- type
+- target_url
+- description
+- artifacts_file
+- artifacts_file_store
+- artifacts_metadata
+- artifacts_metadata_store
+- erased_by_id
+- erased_at
+- artifacts_expire_at
+- environment
+- artifacts_size
+- when
+- yaml_variables
+- queued_at
+- token
+- lock_version
+- coverage_regex
+- auto_canceled_by_id
+- retried
+- protected
+- failure_reason
+- scheduled_at
+- upstream_pipeline_id
+- interruptible
+- processed
+- scheduling_type
+GenericCommitStatus:
- id
- project_id
- status
diff --git a/spec/lib/gitlab/instrumentation/redis_base_spec.rb b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
index 656e6ffba05..426997f6e86 100644
--- a/spec/lib/gitlab/instrumentation/redis_base_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
@@ -210,4 +210,16 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
end
end
end
+
+ describe '.log_exception' do
+ it 'logs exception with storage details' do
+ expect(::Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(
+ an_instance_of(StandardError),
+ storage: instrumentation_class_a.storage_key
+ )
+
+ instrumentation_class_a.log_exception(StandardError.new)
+ end
+ end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 187a6ff1739..18301f01a30 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -64,16 +64,34 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
end
end
- it 'counts exceptions' do
- expect(instrumentation_class).to receive(:instance_count_exception)
- .with(instance_of(Redis::CommandError)).and_call_original
- expect(instrumentation_class).to receive(:instance_count_request).and_call_original
+ context 'when encountering exceptions' do
+ where(:case_name, :exception, :exception_counter) do
+ 'generic exception' | Redis::CommandError | :instance_count_exception
+ 'moved redirection' | Redis::CommandError.new("MOVED 123 127.0.0.1:6380") | :instance_count_cluster_redirection
+ 'ask redirection' | Redis::CommandError.new("ASK 123 127.0.0.1:6380") | :instance_count_cluster_redirection
+ end
- expect do
- Gitlab::Redis::SharedState.with do |redis|
- redis.call(:auth, 'foo', 'bar')
+ with_them do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ # We need to go 1 layer deeper to stub _client as we monkey-patch Redis::Client
+ # with the interceptor. Stubbing `redis` will skip the instrumentation_class.
+ allow(redis._client).to receive(:process).and_raise(exception)
+ end
end
- end.to raise_exception(Redis::CommandError)
+
+ it 'counts exception' do
+ expect(instrumentation_class).to receive(exception_counter)
+ .with(instance_of(Redis::CommandError)).and_call_original
+ expect(instrumentation_class).to receive(:log_exception)
+ .with(instance_of(Redis::CommandError)).and_call_original
+ expect(instrumentation_class).to receive(:instance_count_request).and_call_original
+
+ expect do
+ Gitlab::Redis::SharedState.with { |redis| redis.call(:auth, 'foo', 'bar') }
+ end.to raise_exception(Redis::CommandError)
+ end
+ end
end
context 'in production environment' do
diff --git a/spec/lib/gitlab/internal_post_receive/response_spec.rb b/spec/lib/gitlab/internal_post_receive/response_spec.rb
index 23ea5191486..2792cf49d06 100644
--- a/spec/lib/gitlab/internal_post_receive/response_spec.rb
+++ b/spec/lib/gitlab/internal_post_receive/response_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Gitlab::InternalPostReceive::Response do
describe '#add_alert_message' do
context 'when text is present' do
- it 'adds a alert message' do
+ it 'adds an alert message' do
subject.add_alert_message('hello')
expect(subject.messages.first.message).to eq('hello')
diff --git a/spec/lib/gitlab/issuable_sorter_spec.rb b/spec/lib/gitlab/issuable_sorter_spec.rb
index b8d0c7b0609..0d9940bab6f 100644
--- a/spec/lib/gitlab/issuable_sorter_spec.rb
+++ b/spec/lib/gitlab/issuable_sorter_spec.rb
@@ -4,16 +4,42 @@ require 'spec_helper'
RSpec.describe Gitlab::IssuableSorter do
let(:namespace1) { build_stubbed(:namespace, id: 1) }
- let(:project1) { build_stubbed(:project, id: 1, namespace: namespace1) }
-
- let(:project2) { build_stubbed(:project, id: 2, path: "a", namespace: project1.namespace) }
- let(:project3) { build_stubbed(:project, id: 3, path: "b", namespace: project1.namespace) }
-
let(:namespace2) { build_stubbed(:namespace, id: 2, path: "a") }
let(:namespace3) { build_stubbed(:namespace, id: 3, path: "b") }
- let(:project4) { build_stubbed(:project, id: 4, path: "a", namespace: namespace2) }
- let(:project5) { build_stubbed(:project, id: 5, path: "b", namespace: namespace2) }
- let(:project6) { build_stubbed(:project, id: 6, path: "a", namespace: namespace3) }
+
+ let(:project1) do
+ build_stubbed(:project, id: 1, namespace: namespace1, project_namespace: build_stubbed(:project_namespace))
+ end
+
+ let(:project2) do
+ build_stubbed(
+ :project, id: 2, path: "a", namespace: project1.namespace, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project3) do
+ build_stubbed(
+ :project, id: 3, path: "b", namespace: project1.namespace, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project4) do
+ build_stubbed(
+ :project, id: 4, path: "a", namespace: namespace2, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project5) do
+ build_stubbed(
+ :project, id: 5, path: "b", namespace: namespace2, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
+
+ let(:project6) do
+ build_stubbed(
+ :project, id: 6, path: "a", namespace: namespace3, project_namespace: build_stubbed(:project_namespace)
+ )
+ end
let(:unsorted) { [sorted[2], sorted[3], sorted[0], sorted[1]] }
diff --git a/spec/lib/gitlab/jira_import/issues_importer_spec.rb b/spec/lib/gitlab/jira_import/issues_importer_spec.rb
index 9f654bbcd15..36135c56dd9 100644
--- a/spec/lib/gitlab/jira_import/issues_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issues_importer_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do
def mock_issue_serializer(count, raise_exception_on_even_mocks: false)
serializer = instance_double(Gitlab::JiraImport::IssueSerializer, execute: { key: 'data' })
- allow(Issue).to receive(:with_project_iid_supply).and_return('issue_iid')
+ allow(Issue).to receive(:with_namespace_iid_supply).and_return('issue_iid')
count.times do |i|
if raise_exception_on_even_mocks && i.even?
diff --git a/spec/lib/gitlab/kas/user_access_spec.rb b/spec/lib/gitlab/kas/user_access_spec.rb
new file mode 100644
index 00000000000..8795ad565d0
--- /dev/null
+++ b/spec/lib/gitlab/kas/user_access_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kas::UserAccess, feature_category: :kubernetes_management do
+ describe '.enabled?' do
+ subject { described_class.enabled? }
+
+ before do
+ allow(::Gitlab::Kas).to receive(:enabled?).and_return true
+ end
+
+ it { is_expected.to be true }
+
+ context 'when flag kas_user_access is disabled' do
+ before do
+ stub_feature_flags(kas_user_access: false)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '.enabled_for?' do
+ subject { described_class.enabled_for?(agent) }
+
+ let(:agent) { build(:cluster_agent) }
+
+ before do
+ allow(::Gitlab::Kas).to receive(:enabled?).and_return true
+ end
+
+ it { is_expected.to be true }
+
+ context 'when flag kas_user_access is disabled' do
+ before do
+ stub_feature_flags(kas_user_access: false)
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when flag kas_user_access_project is disabled' do
+ before do
+ stub_feature_flags(kas_user_access_project: false)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '.{encrypt,decrypt}_public_session_id' do
+ let(:data) { 'the data' }
+ let(:encrypted) { described_class.encrypt_public_session_id(data) }
+ let(:decrypted) { described_class.decrypt_public_session_id(encrypted) }
+
+ it { expect(encrypted).not_to include data }
+ it { expect(decrypted).to eq data }
+ end
+
+ describe '.cookie_data' do
+ subject(:cookie_data) { described_class.cookie_data(public_session_id) }
+
+ let(:public_session_id) { 'the-public-session-id' }
+ let(:external_k8s_proxy_url) { 'https://example.com:1234' }
+
+ before do
+ stub_config(
+ gitlab: { host: 'example.com', https: true },
+ gitlab_kas: { external_k8s_proxy_url: external_k8s_proxy_url }
+ )
+ end
+
+ it 'is encrypted, secure, httponly', :aggregate_failures do
+ expect(cookie_data[:value]).not_to include public_session_id
+ expect(cookie_data).to include(httponly: true, secure: true, path: '/')
+ expect(cookie_data).not_to have_key(:domain)
+ end
+
+ context 'when on non-root path' do
+ let(:external_k8s_proxy_url) { 'https://example.com/k8s-proxy' }
+
+ it 'sets :path' do
+ expect(cookie_data).to include(httponly: true, secure: true, path: '/k8s-proxy')
+ end
+ end
+
+ context 'when on subdomain' do
+ let(:external_k8s_proxy_url) { 'https://k8s-proxy.example.com' }
+
+ it 'sets :domain' do
+ expect(cookie_data[:domain]).to eq "example.com"
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kroki_spec.rb b/spec/lib/gitlab/kroki_spec.rb
index 3d6ecf20377..6d8e6ecbf54 100644
--- a/spec/lib/gitlab/kroki_spec.rb
+++ b/spec/lib/gitlab/kroki_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Kroki do
describe '.formats' do
def default_formats
- %w[bytefield c4plantuml ditaa erd graphviz nomnoml pikchr plantuml
+ %w[bytefield c4plantuml d2 dbml diagramsnet ditaa erd graphviz nomnoml pikchr plantuml
structurizr svgbob umlet vega vegalite wavedrom].freeze
end
diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb
index 2d0d205ffb1..ebc2202921b 100644
--- a/spec/lib/gitlab/kubernetes/config_map_spec.rb
+++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb
@@ -4,20 +4,23 @@ require 'spec_helper'
RSpec.describe Gitlab::Kubernetes::ConfigMap do
let(:kubeclient) { double('kubernetes client') }
- let(:application) { create(:clusters_applications_prometheus) }
- let(:config_map) { described_class.new(application.name, application.files) }
+ let(:name) { 'my-name' }
+ let(:files) { [] }
+ let(:config_map) { described_class.new(name, files) }
let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
let(:metadata) do
{
- name: "values-content-configuration-#{application.name}",
+ name: "values-content-configuration-#{name}",
namespace: namespace,
- labels: { name: "values-content-configuration-#{application.name}" }
+ labels: { name: "values-content-configuration-#{name}" }
}
end
describe '#generate' do
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) }
+ let(:resource) do
+ ::Kubeclient::Resource.new(metadata: metadata, data: files)
+ end
subject { config_map.generate }
@@ -28,7 +31,8 @@ RSpec.describe Gitlab::Kubernetes::ConfigMap do
describe '#config_map_name' do
it 'returns the config_map name' do
- expect(config_map.config_map_name).to eq("values-content-configuration-#{application.name}")
+ expect(config_map.config_map_name)
+ .to eq("values-content-configuration-#{name}")
end
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index e3763977add..8aa755bffce 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Kubernetes::Helm::Pod do
with_them do
let(:cluster) { create(:cluster, helm_major_version: helm_major_version) }
- let(:app) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let(:app) { create(:clusters_applications_knative, cluster: cluster) }
let(:command) { app.install_command }
let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
let(:service_account_name) { nil }
diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
index cd66b93eb8b..bb38f4b1bca 100644
--- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::LegacyGithubImport::Importer do
+RSpec.describe Gitlab::LegacyGithubImport::Importer, feature_category: :importers do
+ subject(:importer) { described_class.new(project) }
+
shared_examples 'Gitlab::LegacyGithubImport::Importer#execute' do
let(:expected_not_called) { [] }
@@ -11,8 +13,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
end
it 'calls import methods' do
- importer = described_class.new(project)
-
expected_called = [
:import_labels, :import_milestones, :import_pull_requests, :import_issues,
:import_wiki, :import_releases, :handle_errors,
@@ -51,11 +51,13 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
- allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request_missing_source_branch])
allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_raise(Octokit::NotFound)
allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
+
+ allow(importer).to receive(:restore_source_branch).and_raise(StandardError, 'Some error')
end
let(:label1) do
@@ -153,8 +155,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
}
end
- subject { described_class.new(project) }
-
it 'returns true' do
expect(subject.execute).to eq true
end
@@ -163,18 +163,19 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
expect { subject.execute }.not_to raise_error
end
- it 'stores error messages' do
+ it 'stores error messages', :unlimited_max_formatted_output_length do
error = {
message: 'The remote data could not be fully imported.',
errors: [
{ type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
+ { type: :pull_request, url: "#{api_root}/repos/octocat/Hello-World/pulls/1347", errors: 'Some error' },
{ type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
{ type: :issues_comments, errors: 'Octokit::NotFound' },
{ type: :wiki, errors: "Gitlab::Git::CommandError" }
]
}
- described_class.new(project).execute
+ importer.execute
expect(project.import_state.last_error).to eq error.to_json
end
@@ -182,8 +183,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
shared_examples 'Gitlab::LegacyGithubImport unit-testing' do
describe '#clean_up_restored_branches' do
- subject { described_class.new(project) }
-
before do
allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false }
allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false }
@@ -240,6 +239,16 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
}
end
+ let(:pull_request_missing_source_branch) do
+ pull_request.merge(
+ head: {
+ ref: 'missing',
+ repo: repository,
+ sha: RepoHelpers.another_sample_commit
+ }
+ )
+ end
+
let(:closed_pull_request) do
{
number: 1347,
@@ -264,8 +273,6 @@ RSpec.describe Gitlab::LegacyGithubImport::Importer do
let(:api_root) { 'https://try.gitea.io/api/v1' }
let(:repo_root) { 'https://try.gitea.io' }
- subject { described_class.new(project) }
-
before do
project.update!(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
end
diff --git a/spec/lib/gitlab/loggable_spec.rb b/spec/lib/gitlab/loggable_spec.rb
new file mode 100644
index 00000000000..8238e47014b
--- /dev/null
+++ b/spec/lib/gitlab/loggable_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Loggable, feature_category: :logging do
+ subject(:klass_instance) do
+ Class.new do
+ include Gitlab::Loggable
+
+ def self.name
+ 'MyTestClass'
+ end
+ end.new
+ end
+
+ describe '#build_structured_payload' do
+ it 'adds class and returns formatted json' do
+ expected = {
+ 'class' => 'MyTestClass',
+ 'message' => 'test'
+ }
+
+ expect(klass_instance.build_structured_payload(message: 'test')).to eq(expected)
+ end
+
+ it 'appends additional params and returns formatted json' do
+ expected = {
+ 'class' => 'MyTestClass',
+ 'message' => 'test',
+ 'extra_param' => 1
+ }
+
+ expect(klass_instance.build_structured_payload(message: 'test', extra_param: 1)).to eq(expected)
+ end
+
+ it 'does not raise an error in loggers when passed non-symbols' do
+ expected = {
+ 'class' => 'MyTestClass',
+ 'message' => 'test',
+ '["hello", "thing"]' => :world
+ }
+
+ payload = klass_instance.build_structured_payload(message: 'test', %w[hello thing] => :world)
+ expect(payload).to eq(expected)
+ expect { Gitlab::Export::Logger.info(payload) }.not_to raise_error
+ end
+
+ it 'handles anonymous classes' do
+ anonymous_klass_instance = Class.new { include Gitlab::Loggable }.new
+
+ expected = {
+ 'class' => '<Anonymous>',
+ 'message' => 'test'
+ }
+
+ expect(anonymous_klass_instance.build_structured_payload(message: 'test')).to eq(expected)
+ end
+
+ it 'handles duplicate keys' do
+ expected = {
+ 'class' => 'MyTestClass',
+ 'message' => 'test2'
+ }
+
+ expect(klass_instance.build_structured_payload(message: 'test', 'message' => 'test2')).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
index 4780b1eba53..67d185fd2f1 100644
--- a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'prometheus/client'
require 'support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples'
-RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit do
+RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit, feature_category: :application_performance do
let(:max_rss_limit_gauge) { instance_double(::Prometheus::Client::Gauge) }
let(:memory_limit_bytes) { 2_097_152_000 }
let(:worker_memory_bytes) { 1_048_576_000 }
diff --git a/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb b/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb
index 8a17fa8dd2e..3175c0a6b32 100644
--- a/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb
+++ b/spec/lib/gitlab/metrics/boot_time_tracker_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::Metrics::BootTimeTracker do
+RSpec.describe Gitlab::Metrics::BootTimeTracker, feature_category: :metrics do
let(:logger) { double('logger') }
let(:gauge) { double('gauge') }
diff --git a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
index 9f939d0d7d6..13965bf1244 100644
--- a/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rack_attack_spec.rb
@@ -32,33 +32,6 @@ RSpec.describe Gitlab::Metrics::Subscribers::RackAttack, :request_store do
end
end
- describe '#redis' do
- it 'accumulates per-request RackAttack cache usage' do
- freeze_time do
- subscriber.redis(
- ActiveSupport::Notifications::Event.new(
- 'redis.rack_attack', Time.current, Time.current + 1.second, '1', { operation: 'fetch' }
- )
- )
- subscriber.redis(
- ActiveSupport::Notifications::Event.new(
- 'redis.rack_attack', Time.current, Time.current + 2.seconds, '1', { operation: 'write' }
- )
- )
- subscriber.redis(
- ActiveSupport::Notifications::Event.new(
- 'redis.rack_attack', Time.current, Time.current + 3.seconds, '1', { operation: 'read' }
- )
- )
- end
-
- expect(Gitlab::SafeRequestStore[:rack_attack_instrumentation]).to eql(
- rack_attack_redis_count: 3,
- rack_attack_redis_duration_s: 6.0
- )
- end
- end
-
shared_examples 'log into auth logger' do
context 'when matched throttle does not require user information' do
let(:event) do
diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
index 59bfe2042fa..1731da9b752 100644
--- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
@@ -6,13 +6,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
let(:env) { {} }
let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
-
- let(:event) { double(:event, duration: 15.2, payload: { key: %w[a b c] }) }
+ let(:store) { 'Gitlab::CustomStore' }
+ let(:store_label) { 'CustomStore' }
+ let(:event) { double(:event, duration: 15.2, payload: { key: %w[a b c], store: store }) }
describe '#cache_read' do
it 'increments the cache_read duration' do
expect(subscriber).to receive(:observe)
- .with(:read, event.duration)
+ .with(:read, event)
subscriber.cache_read(event)
end
@@ -27,7 +28,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
let(:event) { double(:event, duration: 15.2, payload: { hit: true }) }
context 'when super operation is fetch' do
- let(:event) { double(:event, duration: 15.2, payload: { hit: true, super_operation: :fetch }) }
+ let(:event) { double(:event, duration: 15.2, payload: { hit: true, super_operation: :fetch, store: store }) }
it 'does not increment cache read miss total' do
expect(transaction).not_to receive(:increment)
@@ -39,7 +40,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
end
context 'with miss event' do
- let(:event) { double(:event, duration: 15.2, payload: { hit: false }) }
+ let(:event) { double(:event, duration: 15.2, payload: { hit: false, store: store }) }
it 'increments the cache_read_miss total' do
expect(transaction).to receive(:increment)
@@ -51,7 +52,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
end
context 'when super operation is fetch' do
- let(:event) { double(:event, duration: 15.2, payload: { hit: false, super_operation: :fetch }) }
+ let(:event) { double(:event, duration: 15.2, payload: { hit: false, super_operation: :fetch, store: store }) }
it 'does not increment cache read miss total' do
expect(transaction).not_to receive(:increment)
@@ -92,7 +93,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
it 'observes read_multi duration' do
expect(subscriber).to receive(:observe)
- .with(:read_multi, event.duration)
+ .with(:read_multi, event)
subject
end
@@ -101,7 +102,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
describe '#cache_write' do
it 'observes write duration' do
expect(subscriber).to receive(:observe)
- .with(:write, event.duration)
+ .with(:write, event)
subscriber.cache_write(event)
end
@@ -110,7 +111,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
describe '#cache_delete' do
it 'observes delete duration' do
expect(subscriber).to receive(:observe)
- .with(:delete, event.duration)
+ .with(:delete, event)
subscriber.cache_delete(event)
end
@@ -119,7 +120,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
describe '#cache_exist?' do
it 'observes the exists duration' do
expect(subscriber).to receive(:observe)
- .with(:exists, event.duration)
+ .with(:exists, event)
subscriber.cache_exist?(event)
end
@@ -179,7 +180,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
it 'returns' do
expect(transaction).not_to receive(:increment)
- subscriber.observe(:foo, 15.2)
+ subscriber.observe(:foo, event)
end
end
@@ -192,17 +193,17 @@ RSpec.describe Gitlab::Metrics::Subscribers::RailsCache do
it 'observes cache metric' do
expect(subscriber.send(:metric_cache_operation_duration_seconds))
.to receive(:observe)
- .with({ operation: :delete }, event.duration / 1000.0)
+ .with({ operation: :delete, store: store_label }, event.duration / 1000.0)
- subscriber.observe(:delete, event.duration)
+ subscriber.observe(:delete, event)
end
it 'increments the operations total' do
expect(transaction)
.to receive(:increment)
- .with(:gitlab_cache_operations_total, 1, { operation: :delete })
+ .with(:gitlab_cache_operations_total, 1, { operation: :delete, store: store_label })
- subscriber.observe(:delete, event.duration)
+ subscriber.observe(:delete, event)
end
end
end
diff --git a/spec/lib/gitlab/monitor/demo_projects_spec.rb b/spec/lib/gitlab/monitor/demo_projects_spec.rb
index 262c78eb62e..6b0f855e38d 100644
--- a/spec/lib/gitlab/monitor/demo_projects_spec.rb
+++ b/spec/lib/gitlab/monitor/demo_projects_spec.rb
@@ -6,15 +6,13 @@ RSpec.describe Gitlab::Monitor::DemoProjects do
describe '#primary_keys' do
subject { described_class.primary_keys }
- it 'fetches primary_keys when on gitlab.com' do
- allow(Gitlab).to receive(:com?).and_return(true)
+ it 'fetches primary_keys when on SaaS', :saas do
allow(Gitlab).to receive(:staging?).and_return(false)
expect(subject).to eq(Gitlab::Monitor::DemoProjects::DOT_COM_IDS)
end
- it 'fetches primary_keys when on staging' do
- allow(Gitlab).to receive(:com?).and_return(true)
+ it 'fetches primary_keys when on staging', :saas do
allow(Gitlab).to receive(:staging?).and_return(true)
expect(subject).to eq(Gitlab::Monitor::DemoProjects::STAGING_IDS)
diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb
index 080b3382684..25baa8913bf 100644
--- a/spec/lib/gitlab/multi_collection_paginator_spec.rb
+++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb
@@ -5,6 +5,13 @@ require 'spec_helper'
RSpec.describe Gitlab::MultiCollectionPaginator do
subject(:paginator) { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 3) }
+ it 'raises an error for invalid page size' do
+ expect { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 0) }
+ .to raise_error(ArgumentError)
+ expect { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: -1) }
+ .to raise_error(ArgumentError)
+ end
+
it 'combines both collections' do
project = create(:project)
group = create(:group)
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 6632a8106ca..1d3452a004a 100644
--- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
+++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
@@ -14,7 +14,8 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem, feature_category: :navigation do
view: 'view',
css_class: 'css_class',
data: {},
- emoji: 'smile'
+ partial: 'groups/some_view_partial_file',
+ component: '_some_component_used_as_a_trigger_for_frontend_dropdown_item_render_'
}
expect(described_class.build(**item)).to eq(item.merge(type: :item))
diff --git a/spec/lib/gitlab/net_http_adapter_spec.rb b/spec/lib/gitlab/net_http_adapter_spec.rb
index fdaf35be31e..cfb90578a4b 100644
--- a/spec/lib/gitlab/net_http_adapter_spec.rb
+++ b/spec/lib/gitlab/net_http_adapter_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'net/http'
-RSpec.describe Gitlab::NetHttpAdapter do
+RSpec.describe Gitlab::NetHttpAdapter, feature_category: :api do
describe '#connect' do
let(:url) { 'https://example.org' }
let(:net_http_adapter) { described_class.new(url) }
diff --git a/spec/lib/gitlab/observability_spec.rb b/spec/lib/gitlab/observability_spec.rb
index 8068d2f8ec9..5082d193197 100644
--- a/spec/lib/gitlab/observability_spec.rb
+++ b/spec/lib/gitlab/observability_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Observability do
+RSpec.describe Gitlab::Observability, feature_category: :error_tracking do
describe '.observability_url' do
let(:gitlab_url) { 'https://example.com' }
@@ -31,29 +31,189 @@ RSpec.describe Gitlab::Observability do
end
end
- describe '.observability_enabled?' do
- let_it_be(:group) { build(:user) }
- let_it_be(:user) { build(:group) }
+ describe '.build_full_url' do
+ let_it_be(:group) { build_stubbed(:group, id: 123) }
+ let(:observability_url) { described_class.observability_url }
+
+ it 'returns the full observability url for the given params' do
+ url = described_class.build_full_url(group, '/foo?bar=baz', '/')
+ expect(url).to eq("https://observe.gitlab.com/-/123/foo?bar=baz")
+ end
+
+ it 'handles missing / from observability_path' do
+ url = described_class.build_full_url(group, 'foo?bar=baz', '/')
+ expect(url).to eq("https://observe.gitlab.com/-/123/foo?bar=baz")
+ end
+
+ it 'sanitises observability_path' do
+ url = described_class.build_full_url(group, "/test?groupId=<script>alert('attack!')</script>", '/')
+ expect(url).to eq("https://observe.gitlab.com/-/123/test?groupId=alert('attack!')")
+ end
+
+ context 'when observability_path is missing' do
+ it 'builds the url with the fallback_path' do
+ url = described_class.build_full_url(group, nil, '/fallback')
+ expect(url).to eq("https://observe.gitlab.com/-/123/fallback")
+ end
+
+ it 'defaults to / if fallback_path is also missing' do
+ url = described_class.build_full_url(group, nil, nil)
+ expect(url).to eq("https://observe.gitlab.com/-/123/")
+ end
+ end
+ end
+
+ describe '.embeddable_url' do
+ before do
+ stub_config_setting(url: "https://www.gitlab.com")
+ # Can't use build/build_stubbed as we want the routes to be generated as well
+ create(:group, path: 'test-path', id: 123)
+ end
+
+ context 'when URL is valid' do
+ where(:input, :expected) do
+ [
+ [
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=%2Fexplore%3FgroupId%3D14485840%26left%3D%255B%2522now-1h%2522,%2522now%2522,%2522new-sentry.gitlab.net%2522,%257B%257D%255D",
+ "https://observe.gitlab.com/-/123/explore?groupId=14485840&left=%5B%22now-1h%22,%22now%22,%22new-sentry.gitlab.net%22,%7B%7D%5D"
+ ],
+ [
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/goto/foo",
+ "https://observe.gitlab.com/-/123/goto/foo"
+ ]
+ ]
+ end
+
+ with_them do
+ it 'returns an embeddable observability url' do
+ expect(described_class.embeddable_url(input)).to eq(expected)
+ end
+ end
+ end
+
+ context 'when URL is invalid' do
+ where(:input) do
+ [
+ # direct links to observe.gitlab.com
+ "https://observe.gitlab.com/-/123/explore",
+ 'https://observe.gitlab.com/v1/auth/start',
+
+ # invalid GitLab URL
+ "not a link",
+ "https://foo.bar/groups/test-path/-/observability/explore?observability_path=/explore",
+ "http://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/explore",
+ "https://www.gitlab.com:123/groups/test-path/-/observability/explore?observability_path=/explore",
+ "https://www.gitlab.com@example.com/groups/test-path/-/observability/explore?observability_path=/explore",
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=@example.com",
+
+ # invalid group/controller/actions
+ "https://www.gitlab.com/groups/INVALID_GROUP/-/observability/explore?observability_path=/explore",
+ "https://www.gitlab.com/groups/test-path/-/INVALID_CONTROLLER/explore?observability_path=/explore",
+ "https://www.gitlab.com/groups/test-path/-/observability/INVALID_ACTION?observability_path=/explore",
+
+ # invalid observablity path
+ "https://www.gitlab.com/groups/test-path/-/observability/explore",
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?missing_observability_path=/explore",
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/not_embeddable",
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=/datasources",
+ "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=not a valid path"
+ ]
+ end
+
+ with_them do
+ it 'returns nil' do
+ expect(described_class.embeddable_url(input)).to be_nil
+ end
+ end
+
+ it 'returns nil if the path detection throws an error' do
+ test_url = "https://www.gitlab.com/groups/test-path/-/observability/explore"
+ allow(Rails.application.routes).to receive(:recognize_path).with(test_url) {
+ raise ActionController::RoutingError, 'test'
+ }
+ expect(described_class.embeddable_url(test_url)).to be_nil
+ end
+
+ it 'returns nil if parsing observaboility path throws an error' do
+ observability_path = 'some-path'
+ test_url = "https://www.gitlab.com/groups/test-path/-/observability/explore?observability_path=#{observability_path}"
+
+ allow(URI).to receive(:parse).and_call_original
+ allow(URI).to receive(:parse).with(observability_path) {
+ raise URI::InvalidURIError, 'test'
+ }
+
+ expect(described_class.embeddable_url(test_url)).to be_nil
+ end
+ end
+ end
+
+ describe '.allowed_for_action?' do
+ let(:group) { build_stubbed(:group) }
+ let(:user) { build_stubbed(:user) }
+
+ before do
+ allow(described_class).to receive(:allowed?).and_call_original
+ end
+
+ it 'returns false if action is nil' do
+ expect(described_class.allowed_for_action?(user, group, nil)).to eq(false)
+ end
+
+ describe 'allowed? calls' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:action, :permission) do
+ :foo | :admin_observability
+ :explore | :read_observability
+ :datasources | :admin_observability
+ :manage | :admin_observability
+ :dashboards | :read_observability
+ end
+
+ with_them do
+ it "calls allowed? with #{params[:permission]} when actions is #{params[:action]}" do
+ described_class.allowed_for_action?(user, group, action)
+ expect(described_class).to have_received(:allowed?).with(user, group, permission)
+ end
+ end
+ end
+ end
+
+ describe '.allowed?' do
+ let(:user) { build_stubbed(:user) }
+ let(:group) { build_stubbed(:group) }
+ let(:test_permission) { :read_observability }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_return(false)
+ end
subject do
- described_class.observability_enabled?(user, group)
+ described_class.allowed?(user, group, test_permission)
end
- it 'checks if read_observability ability is allowed for the given user and group' do
+ it 'checks if ability is allowed for the given user and group' do
allow(Ability).to receive(:allowed?).and_return(true)
subject
- expect(Ability).to have_received(:allowed?).with(user, :read_observability, group)
+ expect(Ability).to have_received(:allowed?).with(user, test_permission, group)
end
- it 'returns true if the read_observability ability is allowed' do
+ it 'checks for admin_observability if permission is missing' do
+ described_class.allowed?(user, group)
+
+ expect(Ability).to have_received(:allowed?).with(user, :admin_observability, group)
+ end
+
+ it 'returns true if the ability is allowed' do
allow(Ability).to receive(:allowed?).and_return(true)
expect(subject).to eq(true)
end
- it 'returns false if the read_observability ability is not allowed' do
+ it 'returns false if the ability is not allowed' do
allow(Ability).to receive(:allowed?).and_return(false)
expect(subject).to eq(false)
@@ -64,5 +224,13 @@ RSpec.describe Gitlab::Observability do
expect(subject).to eq(false)
end
+
+ it 'returns false if group is missing' do
+ expect(described_class.allowed?(user, nil, :read_observability)).to eq(false)
+ end
+
+ it 'returns false if user is missing' do
+ expect(described_class.allowed?(nil, group, :read_observability)).to eq(false)
+ end
end
end
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
index 1d669573b74..34f197b5ddb 100644
--- a/spec/lib/gitlab/optimistic_locking_spec.rb
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -16,6 +16,19 @@ RSpec.describe Gitlab::OptimisticLocking do
describe '#retry_lock' do
let(:name) { 'optimistic_locking_spec' }
+ it 'does not change current_scope', :aggregate_failures do
+ instance = Class.new { include Gitlab::OptimisticLocking }.new
+ relation = pipeline.cancelable_statuses
+
+ expected_scope = Ci::Build.current_scope&.to_sql
+
+ instance.send(:retry_lock, relation, name: :test) do
+ expect(Ci::Build.current_scope&.to_sql).to eq(expected_scope)
+ end
+
+ expect(Ci::Build.current_scope&.to_sql).to eq(expected_scope)
+ end
+
context 'when state changed successfully without retries' do
subject do
described_class.retry_lock(pipeline, name: name) do |lock_subject|
diff --git a/spec/lib/gitlab/pages/random_domain_spec.rb b/spec/lib/gitlab/pages/random_domain_spec.rb
new file mode 100644
index 00000000000..978412bb72c
--- /dev/null
+++ b/spec/lib/gitlab/pages/random_domain_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pages::RandomDomain, feature_category: :pages do
+ let(:namespace_path) { 'namespace' }
+
+ subject(:generator) do
+ described_class.new(project_path: project_path, namespace_path: namespace_path)
+ end
+
+ RSpec.shared_examples 'random domain' do |domain|
+ it do
+ expect(SecureRandom)
+ .to receive(:hex)
+ .and_wrap_original do |_, size, _|
+ ('h' * size)
+ end
+
+ generated = generator.generate
+
+ expect(generated).to eq(domain)
+ expect(generated.length).to eq(63)
+ end
+ end
+
+ context 'when project path is less than 48 chars' do
+ let(:project_path) { 'p' }
+
+ it_behaves_like 'random domain', 'p-namespace-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh'
+ end
+
+ context 'when project path is close to 48 chars' do
+ let(:project_path) { 'p' * 45 }
+
+ it_behaves_like 'random domain', 'ppppppppppppppppppppppppppppppppppppppppppppp-na-hhhhhhhhhhhhhh'
+ end
+
+ context 'when project path is larger than 48 chars' do
+ let(:project_path) { 'p' * 49 }
+
+ it_behaves_like 'random domain', 'pppppppppppppppppppppppppppppppppppppppppppppppp-hhhhhhhhhhhhhh'
+ end
+end
diff --git a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
new file mode 100644
index 00000000000..4b584a45503
--- /dev/null
+++ b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.update_pages_deployment!(create(:pages_deployment, project: project))
+ end
+
+ it 'returns nil when host is empty' do
+ expect(described_class.new(nil).execute).to be_nil
+ expect(described_class.new('').execute).to be_nil
+ end
+
+ context 'when host is a pages custom domain host' do
+ let_it_be(:pages_domain) { create(:pages_domain, project: project) }
+
+ subject(:virtual_domain) { described_class.new(pages_domain.domain).execute }
+
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to be_nil
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+ end
+ end
+ end
+
+ context 'when host is a namespace domain' do
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns no result if the provided host is not subdomain of the Pages host' do
+ virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute
+
+ expect(virtual_domain).to eq(nil)
+ end
+
+ it 'returns the virual domain with no lookup_paths' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(0)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain with no lookup_paths' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}".downcase).execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to be_nil
+ expect(virtual_domain.lookup_paths.length).to eq(0)
+ end
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ project.namespace.update!(path: 'topNAMEspace')
+ end
+
+ it 'returns no result if the provided host is not subdomain of the Pages host' do
+ virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute
+
+ expect(virtual_domain).to eq(nil)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ it 'finds domain with case-insensitive' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host.upcase}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before_all do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to be_nil
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+ end
+ end
+ end
+
+ context 'when host is a unique domain' do
+ before_all do
+ project.project_setting.update!(pages_unique_domain: 'unique-domain')
+ end
+
+ subject(:virtual_domain) { described_class.new("unique-domain.#{Settings.pages.host.upcase}").execute }
+
+ context 'when pages unique domain is enabled' do
+ before_all do
+ project.project_setting.update!(pages_unique_domain_enabled: true)
+ end
+
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+ end
+ end
+ end
+
+ context 'when pages unique domain is disabled' do
+ before_all do
+ project.project_setting.update!(pages_unique_domain_enabled: false)
+ end
+
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/patch/node_loader_spec.rb b/spec/lib/gitlab/patch/node_loader_spec.rb
new file mode 100644
index 00000000000..000083fc6d0
--- /dev/null
+++ b/spec/lib/gitlab/patch/node_loader_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Patch::NodeLoader, feature_category: :redis do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#fetch_node_info' do
+ let(:redis) { double(:redis) } # rubocop:disable RSpec/VerifiedDoubles
+
+ # rubocop:disable Naming/InclusiveLanguage
+ where(:case_name, :args, :value) do
+ [
+ [
+ 'when only ip address is present',
+ "07c37df 127.0.0.1:30004@31004 slave e7d1eec 0 1426238317239 4 connected
+67ed2db 127.0.0.1:30002@31002 master - 0 1426238316232 2 connected 5461-10922
+292f8b3 127.0.0.1:30003@31003 master - 0 1426238318243 3 connected 10923-16383
+6ec2392 127.0.0.1:30005@31005 slave 67ed2db 0 1426238316232 5 connected
+824fe11 127.0.0.1:30006@31006 slave 292f8b3 0 1426238317741 6 connected
+e7d1eec 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-5460",
+ {
+ '127.0.0.1:30004' => 'slave', '127.0.0.1:30002' => 'master', '127.0.0.1:30003' => 'master',
+ '127.0.0.1:30005' => 'slave', '127.0.0.1:30006' => 'slave', '127.0.0.1:30001' => 'master'
+ }
+ ],
+ [
+ 'when hostname is present',
+ "07c37df 127.0.0.1:30004@31004,host1 slave e7d1eec 0 1426238317239 4 connected
+67ed2db 127.0.0.1:30002@31002,host2 master - 0 1426238316232 2 connected 5461-10922
+292f8b3 127.0.0.1:30003@31003,host3 master - 0 1426238318243 3 connected 10923-16383
+6ec2392 127.0.0.1:30005@31005,host4 slave 67ed2db 0 1426238316232 5 connected
+824fe11 127.0.0.1:30006@31006,host5 slave 292f8b3 0 1426238317741 6 connected
+e7d1eec 127.0.0.1:30001@31001,host6 myself,master - 0 0 1 connected 0-5460",
+ {
+ 'host1:30004' => 'slave', 'host2:30002' => 'master', 'host3:30003' => 'master',
+ 'host4:30005' => 'slave', 'host5:30006' => 'slave', 'host6:30001' => 'master'
+ }
+ ],
+ [
+ 'when auxiliary fields are present',
+ "07c37df 127.0.0.1:30004@31004,,shard-id=69bc slave e7d1eec 0 1426238317239 4 connected
+67ed2db 127.0.0.1:30002@31002,,shard-id=114f master - 0 1426238316232 2 connected 5461-10922
+292f8b3 127.0.0.1:30003@31003,,shard-id=fdb3 master - 0 1426238318243 3 connected 10923-16383
+6ec2392 127.0.0.1:30005@31005,,shard-id=114f slave 67ed2db 0 1426238316232 5 connected
+824fe11 127.0.0.1:30006@31006,,shard-id=fdb3 slave 292f8b3 0 1426238317741 6 connected
+e7d1eec 127.0.0.1:30001@31001,,shard-id=69bc myself,master - 0 0 1 connected 0-5460",
+ {
+ '127.0.0.1:30004' => 'slave', '127.0.0.1:30002' => 'master', '127.0.0.1:30003' => 'master',
+ '127.0.0.1:30005' => 'slave', '127.0.0.1:30006' => 'slave', '127.0.0.1:30001' => 'master'
+ }
+ ],
+ [
+ 'when hostname and auxiliary fields are present',
+ "07c37df 127.0.0.1:30004@31004,host1,shard-id=69bc slave e7d1eec 0 1426238317239 4 connected
+67ed2db 127.0.0.1:30002@31002,host2,shard-id=114f master - 0 1426238316232 2 connected 5461-10922
+292f8b3 127.0.0.1:30003@31003,host3,shard-id=fdb3 master - 0 1426238318243 3 connected 10923-16383
+6ec2392 127.0.0.1:30005@31005,host4,shard-id=114f slave 67ed2db 0 1426238316232 5 connected
+824fe11 127.0.0.1:30006@31006,host5,shard-id=fdb3 slave 292f8b3 0 1426238317741 6 connected
+e7d1eec 127.0.0.1:30001@31001,host6,shard-id=69bc myself,master - 0 0 1 connected 0-5460",
+ {
+ 'host1:30004' => 'slave', 'host2:30002' => 'master', 'host3:30003' => 'master',
+ 'host4:30005' => 'slave', 'host5:30006' => 'slave', 'host6:30001' => 'master'
+ }
+ ]
+ ]
+ end
+ # rubocop:enable Naming/InclusiveLanguage
+
+ with_them do
+ before do
+ allow(redis).to receive(:call).with([:cluster, :nodes]).and_return(args)
+ end
+
+ it do
+ expect(Redis::Cluster::NodeLoader.load_flags([redis])).to eq(value)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
deleted file mode 100644
index ff48b9ada90..00000000000
--- a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
- include PrometheusHelpers
-
- let(:project) { create(:project) }
- let(:serverless_func) { ::Serverless::Function.new(project, 'test-name', 'test-ns') }
- let(:client) { double('prometheus_client') }
-
- subject { described_class.new(client) }
-
- context 'verify queries' do
- before do
- create(:prometheus_metric,
- :common,
- identifier: :system_metrics_knative_function_invocation_count,
- query: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))')
- end
-
- it 'has the query, but no data' do
- expect(client).to receive(:query_range).with(
- 'sum(ceil(rate(istio_requests_total{destination_service_namespace="test-ns", destination_service=~"test-name.*"}[1m])*60))',
- hash_including(:start_time, :end_time)
- )
-
- subject.query(serverless_func.id)
- end
- end
-end
diff --git a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb b/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb
deleted file mode 100644
index 8151519ddec..00000000000
--- a/spec/lib/gitlab/rack_attack/instrumented_cache_store_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::RackAttack::InstrumentedCacheStore do
- using RSpec::Parameterized::TableSyntax
-
- let(:store) { ::ActiveSupport::Cache::NullStore.new }
-
- subject { described_class.new(upstream_store: store) }
-
- where(:operation, :params, :test_proc) do
- :fetch | [:key] | ->(s) { s.fetch(:key) }
- :read | [:key] | ->(s) { s.read(:key) }
- :read_multi | [:key_1, :key_2, :key_3] | ->(s) { s.read_multi(:key_1, :key_2, :key_3) }
- :write_multi | [{ key_1: 1, key_2: 2, key_3: 3 }] | ->(s) { s.write_multi(key_1: 1, key_2: 2, key_3: 3) }
- :fetch_multi | [:key_1, :key_2, :key_3] | ->(s) { s.fetch_multi(:key_1, :key_2, :key_3) {} }
- :write | [:key, :value, { option_1: 1 }] | ->(s) { s.write(:key, :value, option_1: 1) }
- :delete | [:key] | ->(s) { s.delete(:key) }
- :exist? | [:key, { option_1: 1 }] | ->(s) { s.exist?(:key, option_1: 1) }
- :delete_matched | [/^key$/, { option_1: 1 }] | ->(s) { s.delete_matched(/^key$/, option_1: 1 ) }
- :increment | [:key, 1] | ->(s) { s.increment(:key, 1) }
- :decrement | [:key, 1] | ->(s) { s.decrement(:key, 1) }
- :cleanup | [] | ->(s) { s.cleanup }
- :clear | [] | ->(s) { s.clear }
- end
-
- with_them do
- it 'publishes a notification' do
- event = nil
-
- begin
- subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args|
- event = ActiveSupport::Notifications::Event.new(*args)
- end
-
- test_proc.call(subject)
- ensure
- ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
- end
-
- expect(event).not_to be_nil
- expect(event.name).to eq("redis.rack_attack")
- expect(event.duration).to be_a(Float).and(be > 0.0)
- expect(event.payload[:operation]).to eql(operation)
- end
-
- it 'publishes a notification even if the cache store returns an error' do
- allow(store).to receive(operation).and_raise('Something went wrong')
-
- event = nil
- exception = nil
-
- begin
- subscriber = ActiveSupport::Notifications.subscribe("redis.rack_attack") do |*args|
- event = ActiveSupport::Notifications::Event.new(*args)
- end
-
- begin
- test_proc.call(subject)
- rescue StandardError => e
- exception = e
- end
- ensure
- ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
- end
-
- expect(event).not_to be_nil
- expect(event.name).to eq("redis.rack_attack")
- expect(event.duration).to be_a(Float).and(be > 0.0)
- expect(event.payload[:operation]).to eql(operation)
-
- expect(exception).not_to be_nil
- expect(exception.message).to eql('Something went wrong')
- end
-
- it 'delegates to the upstream store' do
- allow(store).to receive(operation).and_call_original
-
- if params.empty?
- expect(store).to receive(operation).with(no_args)
- else
- expect(store).to receive(operation).with(*params)
- end
-
- test_proc.call(subject)
- end
- end
-end
diff --git a/spec/lib/gitlab/rack_attack/store_spec.rb b/spec/lib/gitlab/rack_attack/store_spec.rb
new file mode 100644
index 00000000000..19b3f239d91
--- /dev/null
+++ b/spec/lib/gitlab/rack_attack/store_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RackAttack::Store, :clean_gitlab_redis_rate_limiting, feature_category: :scalability do
+ let(:store) { described_class.new }
+ let(:key) { 'foobar' }
+ let(:namespaced_key) { "cache:gitlab:#{key}" }
+
+ def with_redis(&block)
+ Gitlab::Redis::RateLimiting.with(&block)
+ end
+
+ describe '#increment' do
+ it 'increments without expiry' do
+ 5.times do |i|
+ expect(store.increment(key, 1)).to eq(i + 1)
+
+ with_redis do |redis|
+ expect(redis.get(namespaced_key).to_i).to eq(i + 1)
+ expect(redis.ttl(namespaced_key)).to eq(-1)
+ end
+ end
+ end
+
+ it 'rejects amounts other than 1' do
+ expect { store.increment(key, 2) }.to raise_exception(described_class::InvalidAmount)
+ end
+
+ context 'with expiry' do
+ it 'increments and sets expiry' do
+ 5.times do |i|
+ expect(store.increment(key, 1, expires_in: 456)).to eq(i + 1)
+
+ with_redis do |redis|
+ expect(redis.get(namespaced_key).to_i).to eq(i + 1)
+ expect(redis.ttl(namespaced_key)).to be_within(10).of(456)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#read' do
+ subject { store.read(key) }
+
+ it 'reads the namespaced key' do
+ with_redis { |r| r.set(namespaced_key, '123') }
+
+ expect(subject).to eq('123')
+ end
+ end
+
+ describe '#write' do
+ subject { store.write(key, '123', options) }
+
+ let(:options) { {} }
+
+ it 'sets the key' do
+ subject
+
+ with_redis do |redis|
+ expect(redis.get(namespaced_key)).to eq('123')
+ expect(redis.ttl(namespaced_key)).to eq(-1)
+ end
+ end
+
+ context 'with expiry' do
+ let(:options) { { expires_in: 456 } }
+
+ it 'sets the key with expiry' do
+ subject
+
+ with_redis do |redis|
+ expect(redis.get(namespaced_key)).to eq('123')
+ expect(redis.ttl(namespaced_key)).to be_within(10).of(456)
+ end
+ end
+ end
+ end
+
+ describe '#delete' do
+ subject { store.delete(key) }
+
+ it { expect(subject).to eq(0) }
+
+ context 'when the key exists' do
+ before do
+ with_redis { |r| r.set(namespaced_key, '123') }
+ end
+
+ it { expect(subject).to eq(1) }
+ end
+ end
+
+ describe '#with' do
+ subject { store.send(:with, &:ping) }
+
+ it { expect(subject).to eq('PONG') }
+
+ context 'when redis is unavailable' do
+ before do
+ broken_redis = Redis.new(
+ url: 'redis://127.0.0.0:0',
+ instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class
+ )
+ allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(broken_redis)
+ end
+
+ it { expect(subject).to eq(nil) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb
index 64615c4d9ad..82ff8a26199 100644
--- a/spec/lib/gitlab/redis/cache_spec.rb
+++ b/spec/lib/gitlab/redis/cache_spec.rb
@@ -26,22 +26,5 @@ RSpec.describe Gitlab::Redis::Cache do
expect(described_class.active_support_config[:expires_in]).to eq(1.day)
end
-
- context 'when encountering an error' do
- let(:cache) { ActiveSupport::Cache::RedisCacheStore.new(**described_class.active_support_config) }
-
- subject { cache.read('x') }
-
- before do
- described_class.with do |redis|
- allow(redis).to receive(:get).and_raise(::Redis::CommandError)
- end
- end
-
- it 'logs error' do
- expect(::Gitlab::ErrorTracking).to receive(:log_exception)
- subject
- end
- end
end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 423a7e80ead..baf2546fc5c 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -210,47 +210,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- RSpec.shared_examples_for 'fallback read from the non-default store' do
- let(:counter) { Gitlab::Metrics::NullMetric.instance }
-
- before do
- allow(Gitlab::Metrics).to receive(:counter).and_return(counter)
- end
-
- it 'fallback and execute on secondary instance' do
- expect(multi_store.fallback_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- it 'logs the ReadFromPrimaryError' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
- an_instance_of(Gitlab::Redis::MultiStore::ReadFromPrimaryError),
- hash_including(command_name: name, instance_name: instance_name)
- )
-
- subject
- end
-
- it 'increment read fallback count metrics' do
- expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
-
- subject
- end
-
- include_examples 'reads correct value'
-
- context 'when fallback read from the secondary instance raises an exception' do
- before do
- allow(multi_store.fallback_store).to receive(name).with(*expected_args).and_raise(StandardError)
- end
-
- it 'fails with exception' do
- expect { subject }.to raise_error(StandardError)
- end
- end
- end
-
RSpec.shared_examples_for 'secondary store' do
it 'execute on the secondary instance' do
expect(secondary_store).to receive(name).with(*expected_args).and_call_original
@@ -283,31 +242,21 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
subject
end
- unless params[:block]
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
- end
-
include_examples 'reads correct value'
end
- context 'when reading from primary instance is raising an exception' do
+ context 'when reading from default instance is raising an exception' do
before do
allow(multi_store.default_store).to receive(name).with(*expected_args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
- it 'logs the exception' do
+ it 'logs the exception and re-raises the error' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name))
- subject
+ expect { subject }.to raise_error(an_instance_of(StandardError))
end
-
- include_examples 'fallback read from the non-default store'
end
context 'when reading from empty default instance' do
@@ -316,7 +265,9 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
multi_store.default_store.flushdb
end
- include_examples 'fallback read from the non-default store'
+ it 'does not call the fallback store' do
+ expect(multi_store.fallback_store).not_to receive(name)
+ end
end
context 'when the command is executed within pipelined block' do
@@ -346,16 +297,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
context 'when block is provided' do
- it 'both stores yields to the block' do
+ it 'only default store yields to the block' do
expect(primary_store).to receive(name).and_yield(value)
- expect(secondary_store).to receive(name).and_yield(value)
+ expect(secondary_store).not_to receive(name).and_yield(value)
subject
end
- it 'both stores to execute' do
+ it 'only default store to execute' do
expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).not_to receive(name).with(*expected_args).and_call_original
subject
end
@@ -421,27 +372,19 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
subject do
multi_store.mget(values) do |v|
multi_store.sadd(skey, v)
- multi_store.scard(skey)
- end
- end
-
- RSpec.shared_examples_for 'primary instance executes block' do
- it 'ensures primary instance is executing the block' do
- expect(primary_store).to receive(:send).with(:mget, values).and_call_original
- expect(primary_store).to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original
- expect(primary_store).to receive(:send).with(:scard, skey).and_call_original
-
- expect(secondary_store).to receive(:send).with(:mget, values).and_call_original
- expect(secondary_store).to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original
- expect(secondary_store).to receive(:send).with(:scard, skey).and_call_original
-
- subject
end
end
context 'when using both stores' do
context 'when primary instance is default store' do
- it_behaves_like 'primary instance executes block'
+ it 'ensures primary instance is executing the block' do
+ expect(primary_store).to receive(:send).with(:mget, values).and_call_original
+ expect(primary_store).to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original
+
+ expect(secondary_store).not_to receive(:send)
+
+ subject
+ end
end
context 'when secondary instance is default store' do
@@ -449,8 +392,14 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
- # multistore read still favours the primary store
- it_behaves_like 'primary instance executes block'
+ it 'ensures secondary instance is executing the block' do
+ expect(primary_store).not_to receive(:send)
+
+ expect(secondary_store).to receive(:send).with(:mget, values).and_call_original
+ expect(secondary_store).to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original
+
+ subject
+ end
end
end
@@ -465,7 +414,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
expect(primary_store).to receive(:send).with(:mget, values).and_call_original
expect(primary_store).to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original
- expect(primary_store).to receive(:send).with(:scard, skey).and_call_original
subject
end
@@ -479,7 +427,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'ensures only secondary instance is executing the block' do
expect(secondary_store).to receive(:send).with(:mget, values).and_call_original
expect(secondary_store).to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original
- expect(secondary_store).to receive(:send).with(:scard, skey).and_call_original
expect(primary_store).not_to receive(:send)
@@ -668,120 +615,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
# rubocop:enable RSpec/MultipleMemoizedHelpers
- context 'with ENUMERATOR_COMMANDS redis commands' do
- let_it_be(:hkey) { "redis:hash" }
- let_it_be(:skey) { "redis:set" }
- let_it_be(:zkey) { "redis:sortedset" }
- let_it_be(:rvalue) { "value1" }
- let_it_be(:scan_kwargs) { { match: 'redis:hash' } }
-
- where(:case_name, :name, :args, :kwargs) do
- 'execute :scan_each command' | :scan_each | nil | ref(:scan_kwargs)
- 'execute :sscan_each command' | :sscan_each | ref(:skey) | {}
- 'execute :hscan_each command' | :hscan_each | ref(:hkey) | {}
- 'execute :zscan_each command' | :zscan_each | ref(:zkey) | {}
- end
-
- before(:all) do
- primary_store.hset(hkey, rvalue, 1)
- primary_store.sadd?(skey, rvalue)
- primary_store.zadd(zkey, 1, rvalue)
-
- secondary_store.hset(hkey, rvalue, 1)
- secondary_store.sadd?(skey, rvalue)
- secondary_store.zadd(zkey, 1, rvalue)
- end
-
- RSpec.shared_examples_for 'enumerator commands execution' do |both_stores, default_primary|
- context 'without block passed in' do
- subject do
- multi_store.send(name, *args, **kwargs)
- end
-
- it 'returns an enumerator' do
- expect(subject).to be_instance_of(Enumerator)
- end
- end
-
- context 'with block passed in' do
- subject do
- multi_store.send(name, *args, **kwargs) { |key| multi_store.incr(rvalue) }
- end
-
- it 'returns nil' do
- expect(subject).to eq(nil)
- end
-
- it 'runs block on correct Redis instance' do
- if both_stores
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- expect(primary_store).to receive(:incr).with(rvalue)
- expect(secondary_store).to receive(:incr).with(rvalue)
- elsif default_primary
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(primary_store).to receive(:incr).with(rvalue)
-
- expect(secondary_store).not_to receive(name)
- expect(secondary_store).not_to receive(:incr).with(rvalue)
- else
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(:incr).with(rvalue)
-
- expect(primary_store).not_to receive(name)
- expect(primary_store).not_to receive(:incr).with(rvalue)
- end
-
- subject
- end
- end
- end
-
- with_them do
- describe name.to_s do
- let(:expected_args) { kwargs.present? ? [*args, { **kwargs }] : Array(args) }
-
- before do
- allow(primary_store).to receive(name).and_call_original
- allow(secondary_store).to receive(name).and_call_original
- end
-
- context 'when only using 1 store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'when using secondary store as default' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it_behaves_like 'enumerator commands execution', false, false
- end
-
- context 'when using primary store as default' do
- it_behaves_like 'enumerator commands execution', false, true
- end
- end
-
- context 'when using both stores' do
- context 'when using secondary store as default' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it_behaves_like 'enumerator commands execution', true, false
- end
-
- context 'when using primary store as default' do
- it_behaves_like 'enumerator commands execution', true, true
- end
- end
- end
- end
- end
-
RSpec.shared_examples_for 'pipelined command' do |name|
let_it_be(:key1) { "redis:{1}:key_a" }
let_it_be(:value1) { "redis_value1" }
diff --git a/spec/lib/gitlab/redis/rate_limiting_spec.rb b/spec/lib/gitlab/redis/rate_limiting_spec.rb
index d82228426f0..0bea7f8bcb2 100644
--- a/spec/lib/gitlab/redis/rate_limiting_spec.rb
+++ b/spec/lib/gitlab/redis/rate_limiting_spec.rb
@@ -6,19 +6,8 @@ RSpec.describe Gitlab::Redis::RateLimiting do
include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache
describe '.cache_store' do
- context 'when encountering an error' do
- subject { described_class.cache_store.read('x') }
-
- before do
- described_class.with do |redis|
- allow(redis).to receive(:get).and_raise(::Redis::CommandError)
- end
- end
-
- it 'logs error' do
- expect(::Gitlab::ErrorTracking).to receive(:log_exception)
- subject
- end
+ it 'uses the CACHE_NAMESPACE namespace' do
+ expect(described_class.cache_store.options[:namespace]).to eq(Gitlab::Redis::Cache::CACHE_NAMESPACE)
end
end
end
diff --git a/spec/lib/gitlab/redis/repository_cache_spec.rb b/spec/lib/gitlab/redis/repository_cache_spec.rb
index 2c167a6eb62..8cdc4580f9e 100644
--- a/spec/lib/gitlab/redis/repository_cache_spec.rb
+++ b/spec/lib/gitlab/redis/repository_cache_spec.rb
@@ -17,20 +17,5 @@ RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do
it 'has a default ttl of 8 hours' do
expect(described_class.cache_store.options[:expires_in]).to eq(8.hours)
end
-
- context 'when encountering an error' do
- subject { described_class.cache_store.read('x') }
-
- before do
- described_class.with do |redis|
- allow(redis).to receive(:get).and_raise(::Redis::CommandError)
- end
- end
-
- it 'logs error' do
- expect(::Gitlab::ErrorTracking).to receive(:log_exception)
- subject
- end
- end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 31de4068bc5..d885051b93b 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -110,6 +110,8 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to match('.source/.full/.path') }
it { is_expected.to match('domain_namespace') }
it { is_expected.to match('gitlab-migration-test') }
+ it { is_expected.to match('1-project-path') }
+ it { is_expected.to match('e-project-path') }
it { is_expected.to match('') } # it is possible to pass an empty string for destination_namespace in bulk_import POST request
end
@@ -710,6 +712,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to match('libsample0_1.2.3~alpha2_amd64.deb') }
it { is_expected.to match('sample-dev_1.2.3~binary_amd64.deb') }
it { is_expected.to match('sample-udeb_1.2.3~alpha2_amd64.udeb') }
+ it { is_expected.to match('sample-ddeb_1.2.3~alpha2_amd64.ddeb') }
it { is_expected.not_to match('sample_1.2.3~alpha2_amd64.buildinfo') }
it { is_expected.not_to match('sample_1.2.3~alpha2_amd64.changes') }
@@ -1015,6 +1018,34 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.not_to match('/api/v4/groups/1234/packages/debian/pool/compon/a/pkg/file.name') }
end
+ describe 'Packages::MAVEN_SNAPSHOT_DYNAMIC_PARTS' do
+ subject { described_class::Packages::MAVEN_SNAPSHOT_DYNAMIC_PARTS }
+
+ it { is_expected.to match('test-2.11-20230303.163304-1.jar') }
+ it { is_expected.to match('test-2.11-20230303.163304-1-javadoc.jar') }
+ it { is_expected.to match('test-2.11-20230303.163304-1-sources.jar') }
+ it { is_expected.to match('test-2.11-20230303.163304-1-20230303.163304-1.jar') }
+ it { is_expected.to match('test-2.11-20230303.163304-1-20230303.163304-1-javadoc.jar') }
+ it { is_expected.to match('test-2.11-20230303.163304-1-20230303.163304-1-sources.jar') }
+ it { is_expected.to match("#{'a' * 500}-20230303.163304-1-sources.jar") }
+ it { is_expected.to match("test-2.11-20230303.163304-1-#{'a' * 500}.jar") }
+ it { is_expected.to match("#{'a' * 500}-20230303.163304-1-#{'a' * 500}.jar") }
+
+ it { is_expected.not_to match('') }
+ it { is_expected.not_to match(nil) }
+ it { is_expected.not_to match('test') }
+ it { is_expected.not_to match('1.2.3') }
+ it { is_expected.not_to match('1.2.3-javadoc.jar') }
+ it { is_expected.not_to match('-202303039.163304-1.jar') }
+ it { is_expected.not_to match('test-2.11-202303039.163304-1.jar') }
+ it { is_expected.not_to match('test-2.11-20230303.16330-1.jar') }
+ it { is_expected.not_to match('test-2.11-202303039.163304.jar') }
+ it { is_expected.not_to match('test-2.11-202303039.163304-.jar') }
+ it { is_expected.not_to match("#{'a' * 2000}-20230303.163304-1-sources.jar") }
+ it { is_expected.not_to match("test-2.11-20230303.163304-1-#{'a' * 2000}.jar") }
+ it { is_expected.not_to match("#{'a' * 2000}-20230303.163304-1-#{'a' * 2000}.jar") }
+ end
+
describe '.composer_package_version_regex' do
subject { described_class.composer_package_version_regex }
diff --git a/spec/lib/gitlab/safe_device_detector_spec.rb b/spec/lib/gitlab/safe_device_detector_spec.rb
index c37dc1e1c7e..56ba084c435 100644
--- a/spec/lib/gitlab/safe_device_detector_spec.rb
+++ b/spec/lib/gitlab/safe_device_detector_spec.rb
@@ -4,7 +4,7 @@ require 'fast_spec_helper'
require 'device_detector'
require_relative '../../../lib/gitlab/safe_device_detector'
-RSpec.describe Gitlab::SafeDeviceDetector, feature_category: :authentication_and_authorization do
+RSpec.describe Gitlab::SafeDeviceDetector, feature_category: :system_access do
it 'retains the behavior for normal user agents' do
chrome_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
diff --git a/spec/lib/gitlab/sanitizers/exception_message_spec.rb b/spec/lib/gitlab/sanitizers/exception_message_spec.rb
index 8b54b353235..c2c4a5de32d 100644
--- a/spec/lib/gitlab/sanitizers/exception_message_spec.rb
+++ b/spec/lib/gitlab/sanitizers/exception_message_spec.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'addressable'
require 'rspec-parameterized'
-RSpec.describe Gitlab::Sanitizers::ExceptionMessage do
+RSpec.describe Gitlab::Sanitizers::ExceptionMessage, feature_category: :compliance_management do
describe '.clean' do
let(:exception_name) { exception.class.name }
let(:exception_message) { exception.message }
diff --git a/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb
index fe52b586d49..4597cc6b315 100644
--- a/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb
+++ b/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb
@@ -67,5 +67,29 @@ RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder, feature_categor
expect(::Ci::Build.where(runner_id: project[:runner_ids])).to be_empty
end
end
+
+ context 'when number of group runners exceeds plan limit' do
+ before do
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
+ end
+
+ it { is_expected.to be_nil }
+
+ it 'does not change runner count' do
+ expect { seed }.not_to change { Ci::Runner.count }
+ end
+ end
+
+ context 'when number of project runners exceeds plan limit' do
+ before do
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
+ end
+
+ it { is_expected.to be_nil }
+
+ it 'does not change runner count' do
+ expect { seed }.not_to change { Ci::Runner.count }
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/serverless/service_spec.rb b/spec/lib/gitlab/serverless/service_spec.rb
deleted file mode 100644
index 3400be5b48e..00000000000
--- a/spec/lib/gitlab/serverless/service_spec.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Serverless::Service do
- let(:cluster) { create(:cluster) }
- let(:environment) { create(:environment) }
- let(:attributes) do
- {
- 'apiVersion' => 'serving.knative.dev/v1alpha1',
- 'kind' => 'Service',
- 'metadata' => {
- 'creationTimestamp' => '2019-10-22T21:19:13Z',
- 'name' => 'kubetest',
- 'namespace' => 'project1-1-environment1'
- },
- 'spec' => {
- 'runLatest' => {
- 'configuration' => {
- 'build' => {
- 'template' => {
- 'name' => 'some-image'
- }
- }
- }
- }
- },
- 'environment_scope' => '*',
- 'cluster' => cluster,
- 'environment' => environment,
- 'podcount' => 0
- }
- end
-
- it 'exposes methods extracting data from the attributes hash' do
- service = Gitlab::Serverless::Service.new(attributes)
-
- expect(service.name).to eq('kubetest')
- expect(service.namespace).to eq('project1-1-environment1')
- expect(service.environment_scope).to eq('*')
- expect(service.podcount).to eq(0)
- expect(service.created_at).to eq(DateTime.parse('2019-10-22T21:19:13Z'))
- expect(service.image).to eq('some-image')
- expect(service.cluster).to eq(cluster)
- expect(service.environment).to eq(environment)
- end
-
- it 'returns nil for missing attributes' do
- service = Gitlab::Serverless::Service.new({})
-
- [:name, :namespace, :environment_scope, :cluster, :podcount, :created_at, :image, :description, :url, :environment].each do |method|
- expect(service.send(method)).to be_nil
- end
- end
-
- describe '#description' do
- it 'extracts the description in knative 7 format if available' do
- attributes = {
- 'spec' => {
- 'template' => {
- 'metadata' => {
- 'annotations' => {
- 'Description' => 'some description'
- }
- }
- }
- }
- }
- service = Gitlab::Serverless::Service.new(attributes)
-
- expect(service.description).to eq('some description')
- end
-
- it 'extracts the description in knative 5/6 format if 7 is not available' do
- attributes = {
- 'spec' => {
- 'runLatest' => {
- 'configuration' => {
- 'revisionTemplate' => {
- 'metadata' => {
- 'annotations' => {
- 'Description' => 'some description'
- }
- }
- }
- }
- }
- }
- }
- service = Gitlab::Serverless::Service.new(attributes)
-
- expect(service.description).to eq('some description')
- end
- end
-
- describe '#url' do
- let(:serverless_domain) { instance_double(::Serverless::Domain, uri: URI('https://proxy.example.com')) }
-
- it 'returns proxy URL if cluster has serverless domain' do
- # cluster = create(:cluster)
- knative = create(:clusters_applications_knative, :installed, cluster: cluster)
- create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id)
- service = Gitlab::Serverless::Service.new(attributes.merge('cluster' => cluster))
-
- expect(::Serverless::Domain).to receive(:new).with(
- function_name: service.name,
- serverless_domain_cluster: service.cluster.serverless_domain,
- environment: service.environment
- ).and_return(serverless_domain)
-
- expect(service.url).to eq('https://proxy.example.com')
- end
-
- it 'returns the URL from the knative 6/7 format' do
- attributes = {
- 'status' => {
- 'url' => 'https://example.com'
- }
- }
- service = Gitlab::Serverless::Service.new(attributes)
-
- expect(service.url).to eq('https://example.com')
- end
-
- it 'returns the URL from the knative 5 format' do
- attributes = {
- 'status' => {
- 'domain' => 'example.com'
- }
- }
- service = Gitlab::Serverless::Service.new(attributes)
-
- expect(service.url).to eq('http://example.com')
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index f7cee6beb58..80c1af1b913 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -331,7 +331,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
include_context 'server metrics call'
context 'when a worker has a feature category' do
- let(:worker_category) { 'authentication_and_authorization' }
+ let(:worker_category) { 'system_access' }
it 'uses that category for metrics' do
expect(completion_seconds_metric).to receive(:observe).with(a_hash_including(feature_category: worker_category), anything)
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
index 1b6cd7ac5fb..4fbc64a45d6 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/client_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
context 'when the feature category is already set in the surrounding block' do
it 'takes the feature category from the worker, not the caller' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
@@ -139,7 +139,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
end
it 'takes the feature category from the caller if the worker is not owned' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestNotOwnedWithContextWorker.bulk_perform_async_with_contexts(
%w(job1 job2),
arguments_proc: -> (name) { [name, 1, 2, 3] },
@@ -150,8 +150,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Client do
job1 = TestNotOwnedWithContextWorker.job_for_args(['job1', 1, 2, 3])
job2 = TestNotOwnedWithContextWorker.job_for_args(['job2', 1, 2, 3])
- expect(job1['meta.feature_category']).to eq('authentication_and_authorization')
- expect(job2['meta.feature_category']).to eq('authentication_and_authorization')
+ expect(job1['meta.feature_category']).to eq('system_access')
+ expect(job2['meta.feature_category']).to eq('system_access')
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
index 2deab3064eb..eb077a0371c 100644
--- a/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/worker_context/server_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
context 'feature category' do
it 'takes the feature category from the worker' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
TestWorker.perform_async('identifier', 1)
end
@@ -78,11 +78,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::WorkerContext::Server do
context 'when the worker is not owned' do
it 'takes the feature category from the surrounding context' do
- Gitlab::ApplicationContext.with_context(feature_category: 'authentication_and_authorization') do
+ Gitlab::ApplicationContext.with_context(feature_category: 'system_access') do
NotOwnedWorker.perform_async('identifier', 1)
end
- expect(NotOwnedWorker.contexts['identifier']).to include('meta.feature_category' => 'authentication_and_authorization')
+ expect(NotOwnedWorker.contexts['identifier']).to include('meta.feature_category' => 'system_access')
end
end
end
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/slug/path_spec.rb b/spec/lib/gitlab/slug/path_spec.rb
index 9a7067e40a2..bbc2a05713d 100644
--- a/spec/lib/gitlab/slug/path_spec.rb
+++ b/spec/lib/gitlab/slug/path_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Gitlab::Slug::Path, feature_category: :not_owned do
+RSpec.describe Gitlab::Slug::Path, feature_category: :shared do
describe '#generate' do
{
'name': 'name',
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 05f7af7606d..912093be29f 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -8,7 +8,9 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:schemes) { %w[http https] }
describe '#validate!' do
- subject { described_class.validate!(import_url, schemes: schemes) }
+ let(:options) { { schemes: schemes } }
+
+ subject { described_class.validate!(import_url, **options) }
shared_examples 'validates URI and hostname' do
it 'runs the url validations' do
@@ -19,6 +21,73 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
end
+ shared_context 'instance configured to deny all requests' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true)
+ stub_application_setting(deny_all_requests_except_allowed: true)
+ end
+ end
+
+ shared_examples 'a URI denied by `deny_all_requests_except_allowed`' do
+ context 'when instance setting is enabled' do
+ include_context 'instance configured to deny all requests'
+
+ it 'blocks the request' do
+ expect { subject }.to raise_error(described_class::BlockedUrlError)
+ end
+ end
+
+ context 'when instance setting is not enabled' do
+ it 'does not block the request' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when passed as an argument' do
+ let(:options) { super().merge(deny_all_requests_except_allowed: arg_value) }
+
+ context 'when argument is a proc that evaluates to true' do
+ let(:arg_value) { proc { true } }
+
+ it 'blocks the request' do
+ expect { subject }.to raise_error(described_class::BlockedUrlError)
+ end
+ end
+
+ context 'when argument is a proc that evaluates to false' do
+ let(:arg_value) { proc { false } }
+
+ it 'does not block the request' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when argument is true' do
+ let(:arg_value) { true }
+
+ it 'blocks the request' do
+ expect { subject }.to raise_error(described_class::BlockedUrlError)
+ end
+ end
+
+ context 'when argument is false' do
+ let(:arg_value) { false }
+
+ it 'does not block the request' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ shared_examples 'a URI exempt from `deny_all_requests_except_allowed`' do
+ include_context 'instance configured to deny all requests'
+
+ it 'does not block the request' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
context 'when URI is nil' do
let(:import_url) { nil }
@@ -26,6 +95,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { nil }
let(:expected_hostname) { nil }
end
+
+ it_behaves_like 'a URI exempt from `deny_all_requests_except_allowed`'
end
context 'when URI is internal' do
@@ -39,6 +110,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { 'http://127.0.0.1' }
let(:expected_hostname) { 'localhost' }
end
+
+ it_behaves_like 'a URI exempt from `deny_all_requests_except_allowed`'
end
context 'when URI is for a local object storage' do
@@ -61,7 +134,7 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end
context 'when allow_object_storage is true' do
- subject { described_class.validate!(import_url, allow_object_storage: true, schemes: schemes) }
+ let(:options) { { allow_object_storage: true, schemes: schemes } }
context 'with a local domain name' do
let(:host) { 'http://review-minio-svc.svc:9000' }
@@ -74,6 +147,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { 'http://127.0.0.1:9000/external-diffs/merge_request_diffs/mr-1/diff-1' }
let(:expected_hostname) { 'review-minio-svc.svc' }
end
+
+ it_behaves_like 'a URI exempt from `deny_all_requests_except_allowed`'
end
context 'with an IP address' do
@@ -83,6 +158,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { 'http://127.0.0.1:9000/external-diffs/merge_request_diffs/mr-1/diff-1' }
let(:expected_hostname) { nil }
end
+
+ it_behaves_like 'a URI exempt from `deny_all_requests_except_allowed`'
end
context 'when LFS object storage is enabled' do
@@ -164,6 +241,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { 'https://93.184.216.34' }
let(:expected_hostname) { 'example.org' }
end
+
+ it_behaves_like 'a URI denied by `deny_all_requests_except_allowed`'
end
context 'when domain cannot be resolved' do
@@ -193,6 +272,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_hostname) { nil }
end
+ it_behaves_like 'a URI denied by `deny_all_requests_except_allowed`'
+
context 'when the address is invalid' do
let(:import_url) { 'http://1.1.1.1.1' }
@@ -217,10 +298,12 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { 'http://192.168.0.120:9121/scrape?target=unix:///var/opt/gitlab/redis/redis.socket&amp;check-keys=*' }
let(:expected_hostname) { 'a.192.168.0.120.3times.127.0.0.1.1time.repeat.rebind.network' }
end
+
+ it_behaves_like 'a URI exempt from `deny_all_requests_except_allowed`'
end
context 'disabled DNS rebinding protection' do
- subject { described_class.validate!(import_url, dns_rebind_protection: false, schemes: schemes) }
+ let(:options) { { dns_rebind_protection: false, schemes: schemes } }
context 'when URI is internal' do
let(:import_url) { 'http://localhost' }
@@ -229,6 +312,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { import_url }
let(:expected_hostname) { nil }
end
+
+ it_behaves_like 'a URI exempt from `deny_all_requests_except_allowed`'
end
context 'when the URL hostname is a domain' do
@@ -243,6 +328,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { import_url }
let(:expected_hostname) { nil }
end
+
+ it_behaves_like 'a URI denied by `deny_all_requests_except_allowed`'
end
context 'when domain cannot be resolved' do
@@ -252,6 +339,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { import_url }
let(:expected_hostname) { nil }
end
+
+ it_behaves_like 'a URI denied by `deny_all_requests_except_allowed`'
end
end
@@ -263,6 +352,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_hostname) { nil }
end
+ it_behaves_like 'a URI denied by `deny_all_requests_except_allowed`'
+
context 'when it is invalid' do
let(:import_url) { 'http://1.1.1.1.1' }
@@ -270,6 +361,8 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
let(:expected_uri) { import_url }
let(:expected_hostname) { nil }
end
+
+ it_behaves_like 'a URI denied by `deny_all_requests_except_allowed`'
end
end
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 2e9a444bd24..08a25666ae9 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -227,27 +227,5 @@ RSpec.describe Gitlab::UrlBuilder do
expect(subject.build(object, only_path: true)).to eq("/#{project.full_path}")
end
end
-
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- context 'when a task issue is passed' do
- it 'returns a path using the work item\'s ID and no query params' do
- task = create(:issue, :task)
-
- expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/work_items/#{task.id}")
- end
- end
-
- context 'when a work item is passed' do
- it 'returns a path using the work item\'s ID and no query params' do
- work_item = create(:work_item)
-
- expect(subject.build(work_item, only_path: true)).to eq("/#{work_item.project.full_path}/-/work_items/#{work_item.id}")
- end
- end
- end
end
end
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 ce15d44b1e1..317929f77e6 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitiesMetric do
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitiesMetric, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:bulk_import_projects) do
create_list(:bulk_import_entity, 2, source_type: 'project_entity', created_at: 3.weeks.ago, status: 2)
@@ -163,4 +163,121 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBulkImportsEntitie
options: { status: 2, source_type: 'project_entity' }
end
end
+
+ context 'with has_failures: true' do
+ before(:all) do
+ create_list(:bulk_import_entity, 3, :project_entity, :finished, created_at: 3.weeks.ago, has_failures: true)
+ create_list(:bulk_import_entity, 2, :project_entity, :finished, created_at: 2.months.ago, has_failures: true)
+ create_list(:bulk_import_entity, 3, :group_entity, :finished, created_at: 3.weeks.ago, has_failures: true)
+ create_list(:bulk_import_entity, 2, :group_entity, :finished, created_at: 2.months.ago, has_failures: true)
+ end
+
+ context 'with all time frame' do
+ context 'with project entity' do
+ let(:expected_value) { 5 }
+ 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 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = TRUE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: 'all',
+ options: { status: 2, source_type: 'project_entity', has_failures: true }
+ end
+
+ context 'with group entity' do
+ let(:expected_value) { 5 }
+ let(:expected_query) do
+ "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\" " \
+ "WHERE \"bulk_import_entities\".\"source_type\" = 0 AND \"bulk_import_entities\".\"status\" = 2 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = TRUE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: 'all',
+ options: { status: 2, source_type: 'group_entity', has_failures: true }
+ end
+ end
+
+ context 'for 28d time frame' do
+ let(:expected_value) { 3 }
+ 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 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = TRUE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: '28d',
+ options: { status: 2, source_type: 'project_entity', has_failures: true }
+ end
+ end
+
+ context 'with has_failures: false' do
+ context 'with all time frame' do
+ context 'with project entity' 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 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = FALSE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: 'all',
+ options: { status: 2, source_type: 'project_entity', has_failures: false }
+ end
+
+ context 'with group entity' do
+ let(:expected_value) { 2 }
+ let(:expected_query) do
+ "SELECT COUNT(\"bulk_import_entities\".\"id\") FROM \"bulk_import_entities\" " \
+ "WHERE \"bulk_import_entities\".\"source_type\" = 0 AND \"bulk_import_entities\".\"status\" = 2 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = FALSE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: 'all',
+ options: { status: 2, source_type: 'group_entity', has_failures: false }
+ end
+ end
+
+ context 'for 28d time frame' do
+ context 'with project entity' 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 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = FALSE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: '28d',
+ options: { status: 2, source_type: 'project_entity', has_failures: false }
+ end
+
+ context 'with group entity' 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\" = 0 AND \"bulk_import_entities\".\"status\" = 2 " \
+ "AND \"bulk_import_entities\".\"has_failures\" = FALSE"
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ time_frame: '28d',
+ options: { status: 2, source_type: 'group_entity', has_failures: false }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
index afd8fccd56c..77c49d448d7 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
@@ -4,25 +4,23 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiInternalPipelinesMetric,
feature_category: :service_ping do
- let_it_be(:ci_pipeline_1) { create(:ci_pipeline, source: :external) }
- let_it_be(:ci_pipeline_2) { create(:ci_pipeline, source: :push) }
-
- let(:expected_value) { 1 }
- let(:expected_query) do
- 'SELECT COUNT("ci_pipelines"."id") FROM "ci_pipelines" ' \
- 'WHERE ("ci_pipelines"."source" IN (1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15) ' \
- 'OR "ci_pipelines"."source" IS NULL)'
- end
+ let_it_be(:ci_pipeline_1) { create(:ci_pipeline, source: :external, created_at: 3.days.ago) }
+ let_it_be(:ci_pipeline_2) { create(:ci_pipeline, source: :push, created_at: 3.days.ago) }
+ let_it_be(:old_pipeline) { create(:ci_pipeline, source: :push, created_at: 2.months.ago) }
+ let_it_be(:expected_value) { 2 }
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
- context 'on Gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
+ context 'for monthly counts' do
+ let_it_be(:expected_value) { 1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' }
+ end
- let(:expected_value) { -1 }
+ context 'on SaaS', :saas do
+ let_it_be(:expected_value) { -1 }
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ 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/count_issues_created_manually_from_alerts_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
index 86f54c48666..65e514bf345 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
@@ -16,11 +16,7 @@ feature_category: :service_ping do
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
- context 'on Gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'on SaaS', :saas do
let(:expected_value) { -1 }
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/gitlab_dedicated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/gitlab_dedicated_metric_spec.rb
new file mode 100644
index 00000000000..a35022ec2c4
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/gitlab_dedicated_metric_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::GitlabDedicatedMetric, feature_category: :service_ping do
+ let(:expected_value) { Gitlab::CurrentSettings.gitlab_dedicated_instance }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb
new file mode 100644
index 00000000000..afc9d610207
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/index_inconsistencies_metric_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::IndexInconsistenciesMetric, feature_category: :database do
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all' } do
+ let(:expected_value) do
+ [
+ { inconsistency_type: 'wrong_indexes', object_name: 'index_name_1' },
+ { inconsistency_type: 'missing_indexes', object_name: 'index_name_2' },
+ { inconsistency_type: 'extra_indexes', object_name: 'index_name_3' }
+ ]
+ end
+
+ let(:runner) { instance_double(Gitlab::Database::SchemaValidation::Runner, execute: inconsistencies) }
+ let(:inconsistency_class) { Gitlab::Database::SchemaValidation::Validators::BaseValidator::Inconsistency }
+
+ let(:inconsistencies) do
+ [
+ instance_double(inconsistency_class, object_name: 'index_name_1', type: 'wrong_indexes'),
+ instance_double(inconsistency_class, object_name: 'index_name_2', type: 'missing_indexes'),
+ instance_double(inconsistency_class, object_name: 'index_name_3', type: 'extra_indexes')
+ ]
+ end
+
+ before do
+ allow(Gitlab::Database::SchemaValidation::Runner).to receive(:new).and_return(runner)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric_spec.rb
new file mode 100644
index 00000000000..ff6be56c13f
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/installation_creation_date_metric_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InstallationCreationDateMetric,
+ feature_category: :service_ping do
+ context 'with a root user' do
+ let_it_be(:root) { create(:user, id: 1) }
+ let_it_be(:expected_value) { root.reload.created_at } # reloading to get the timestamp from the database
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+
+ context 'without a root user' do
+ let_it_be(:another_user) { create(:user, id: 2) }
+ let_it_be(:expected_value) { nil }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb b/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb
index 63a1da490ed..8da86e4fae5 100644
--- a/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/code_review_events_spec.rb
@@ -6,17 +6,23 @@ require 'spec_helper'
# NOTE: ONLY user related metrics to be added to the aggregates - otherwise add it to the exception list
RSpec.describe 'Code review events' do
it 'the aggregated metrics contain all the code review metrics' do
- code_review_events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category("code_review")
+ mr_related_events = %w[i_code_review_create_mr i_code_review_mr_diffs i_code_review_mr_with_invalid_approvers i_code_review_mr_single_file_diffs i_code_review_total_suggestions_applied i_code_review_total_suggestions_added i_code_review_create_note_in_ipynb_diff i_code_review_create_note_in_ipynb_diff_mr i_code_review_create_note_in_ipynb_diff_commit i_code_review_merge_request_widget_license_compliance_warning]
+
+ all_code_review_events = Gitlab::Usage::MetricDefinition.all.flat_map do |definition|
+ next [] unless definition.attributes[:key_path].include?('.code_review.') &&
+ definition.attributes[:status] == 'active' &&
+ definition.attributes[:instrumentation_class] != 'AggregatedMetric'
+
+ definition.attributes.dig(:options, :events)
+ end.uniq.compact
+
code_review_aggregated_events = Gitlab::Usage::MetricDefinition.all.flat_map do |definition|
next [] unless code_review_aggregated_metric?(definition.attributes)
definition.attributes.dig(:options, :events)
end.uniq
- exceptions = %w[i_code_review_create_mr i_code_review_mr_diffs i_code_review_mr_with_invalid_approvers i_code_review_mr_single_file_diffs i_code_review_total_suggestions_applied i_code_review_total_suggestions_added i_code_review_create_note_in_ipynb_diff i_code_review_create_note_in_ipynb_diff_mr i_code_review_create_note_in_ipynb_diff_commit]
- code_review_aggregated_events += exceptions
-
- expect(code_review_events - code_review_aggregated_events).to be_empty
+ expect(all_code_review_events - (code_review_aggregated_events + mr_related_events)).to be_empty
end
def code_review_aggregated_metric?(attributes)
diff --git a/spec/lib/gitlab/usage_data_counters/container_registry_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/container_registry_event_counter_spec.rb
new file mode 100644
index 00000000000..052735db96b
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/container_registry_event_counter_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageDataCounters::ContainerRegistryEventCounter, :clean_gitlab_redis_shared_state,
+ feature_category: :container_registry do
+ described_class::KNOWN_EVENTS.each do |event|
+ it_behaves_like 'a redis usage counter', 'ContainerRegistryEvent', event
+ it_behaves_like 'a redis usage counter with totals', :container_registry_events, "#{event}": 5
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
index f8a4603c1f8..c16d31cd8ef 100644
--- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
@@ -25,12 +25,29 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
end
end
+ it 'track snowplow event' do
+ track_action(author: user1, project: project)
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'ide_edit',
+ label: 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit',
+ namespace: project.namespace,
+ property: event_name,
+ project: project,
+ user: user1,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_h]
+ )
+ end
+
it 'does not track edit actions if author is not present' do
expect(track_action(author: nil, project: project)).to be_nil
end
end
context 'for web IDE edit actions' do
+ let(:event_name) { described_class::EDIT_BY_WEB_IDE }
+
it_behaves_like 'tracks and counts action' do
def track_action(params)
described_class.track_web_ide_edit_action(**params)
@@ -43,6 +60,8 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
end
context 'for SFE edit actions' do
+ let(:event_name) { described_class::EDIT_BY_SFE }
+
it_behaves_like 'tracks and counts action' do
def track_action(params)
described_class.track_sfe_edit_action(**params)
@@ -55,6 +74,8 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
end
context 'for snippet editor edit actions' do
+ let(:event_name) { described_class::EDIT_BY_SNIPPET_EDITOR }
+
it_behaves_like 'tracks and counts action' do
def track_action(params)
described_class.track_snippet_editor_edit_action(**params)
diff --git a/spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb
index d6eb67e5c35..9cbac835a6f 100644
--- a/spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/gitlab_cli_activity_unique_counter_spec.rb
@@ -7,9 +7,18 @@ RSpec.describe Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter, :clean
let(:user2) { build(:user, id: 2) }
let(:time) { Time.current }
let(:action) { described_class::GITLAB_CLI_API_REQUEST_ACTION }
- let(:user_agent) { { user_agent: 'GLab - GitLab CLI' } }
context 'when tracking a gitlab cli request' do
- it_behaves_like 'a request from an extension'
+ context 'with the old UserAgent' do
+ let(:user_agent) { { user_agent: 'GLab - GitLab CLI' } }
+
+ it_behaves_like 'a request from an extension'
+ end
+
+ context 'with the current UserAgent' do
+ let(:user_agent) { { user_agent: 'glab/v1.25.3-27-g7ec258fb (built 2023-02-16), darwin' } }
+
+ it_behaves_like 'a request from an extension'
+ end
end
end
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 f955fd265e5..8c497970555 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
@@ -23,47 +23,12 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
described_class.clear_memoization(:known_events)
end
- describe '.categories' do
- it 'gets CE unique category names' do
- expect(described_class.categories).to include(
- 'analytics',
- 'ci_templates',
- 'ci_users',
- 'code_review',
- 'deploy_token_packages',
- 'ecosystem',
- 'environments',
- 'error_tracking',
- 'geo',
- 'ide_edit',
- 'importer',
- 'incident_management_alerts',
- 'incident_management',
- 'issues_edit',
- 'kubernetes_agent',
- 'manage',
- 'pipeline_authoring',
- 'quickactions',
- 'search',
- 'secure',
- 'snippets',
- 'source_code',
- 'terraform',
- 'testing',
- 'user_packages',
- 'work_items'
- )
- end
- end
-
describe '.known_events' do
let(:ce_temp_dir) { Dir.mktmpdir }
let(:ce_temp_file) { Tempfile.new(%w[common .yml], ce_temp_dir) }
let(:ce_event) do
{
"name" => "ce_event",
- "redis_slot" => "analytics",
- "category" => "analytics",
"aggregation" => "weekly"
}
end
@@ -84,8 +49,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
describe 'known_events' do
- let(:feature) { 'test_hll_redis_counter_ff_check' }
-
let(:weekly_event) { 'g_analytics_contribution' }
let(:daily_event) { 'g_analytics_search' }
let(:analytics_slot_event) { 'g_analytics_contribution' }
@@ -105,13 +68,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: weekly_event, redis_slot: "analytics", category: analytics_category, aggregation: "weekly", feature_flag: feature },
- { name: daily_event, redis_slot: "analytics", category: analytics_category, aggregation: "daily" },
- { name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" },
- { name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" },
- { name: no_slot, category: global_category, aggregation: "daily" },
- { name: different_aggregation, category: global_category, aggregation: "monthly" },
- { name: context_event, category: other_category, aggregation: 'weekly' }
+ { name: weekly_event, aggregation: "weekly" },
+ { name: daily_event, aggregation: "daily" },
+ { name: category_productivity_event, aggregation: "weekly" },
+ { name: compliance_slot_event, aggregation: "weekly" },
+ { name: no_slot, aggregation: "daily" },
+ { name: different_aggregation, aggregation: "monthly" },
+ { name: context_event, aggregation: 'weekly' }
].map(&:with_indifferent_access)
end
@@ -121,12 +84,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
allow(described_class).to receive(:known_events).and_return(known_events)
end
- describe '.events_for_category' do
- it 'gets the event names for given category' do
- expect(described_class.events_for_category(:analytics)).to contain_exactly(weekly_event, daily_event)
- end
- end
-
describe '.track_event' do
context 'with redis_hll_tracking' do
it 'tracks the event when feature enabled' do
@@ -146,32 +103,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- context 'with event feature flag set' do
- it 'tracks the event when feature enabled' do
- stub_feature_flags(feature => true)
-
- expect(Gitlab::Redis::HLL).to receive(:add)
-
- described_class.track_event(weekly_event, values: 1)
- end
-
- it 'does not track the event with feature flag disabled' do
- stub_feature_flags(feature => false)
-
- expect(Gitlab::Redis::HLL).not_to receive(:add)
-
- described_class.track_event(weekly_event, values: 1)
- end
- end
-
- context 'with no event feature flag set' do
- it 'tracks the event' do
- expect(Gitlab::Redis::HLL).to receive(:add)
-
- described_class.track_event(daily_event, values: 1)
- end
- end
-
context 'when usage_ping is disabled' do
it 'does not track the event' do
allow(::ServicePing::ServicePingSettings).to receive(:enabled?).and_return(false)
@@ -195,7 +126,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it 'tracks events with multiple values' do
values = [entity1, entity2]
- expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/, value: values,
+ expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/, value: values,
expiry: described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
described_class.track_event(:g_analytics_contribution, values: values)
@@ -237,7 +168,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
described_class.track_event("g_compliance_dashboard", values: entity1)
Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "g_{compliance}_dashboard-*").to_a
+ keys = redis.scan_each(match: "{#{described_class::REDIS_SLOT}}_g_compliance_dashboard-*").to_a
expect(keys).not_to be_empty
keys.each do |key|
@@ -252,7 +183,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
described_class.track_event("no_slot", values: entity1)
Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "*-{no_slot}").to_a
+ keys = redis.scan_each(match: "*_no_slot").to_a
expect(keys).not_to be_empty
keys.each do |key|
@@ -276,7 +207,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it 'tracks events with multiple values' do
values = [entity1, entity2]
- expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/,
+ expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_analytics_contribution/,
value: values,
expiry: described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
@@ -340,18 +271,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect(described_class.unique_events(event_names: [weekly_event], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1)
end
- it 'raise error if metrics are not in the same slot' do
- expect do
- described_class.unique_events(event_names: [compliance_slot_event, analytics_slot_event], start_date: 4.weeks.ago, end_date: Date.current)
- end.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::SlotMismatch)
- end
-
- it 'raise error if metrics are not in the same category' do
- expect do
- described_class.unique_events(event_names: [category_analytics_event, category_productivity_event], start_date: 4.weeks.ago, end_date: Date.current)
- end.to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::CategoryMismatch)
- end
-
it "raise error if metrics don't have same aggregation" do
expect do
described_class.unique_events(event_names: [daily_event, weekly_event], start_date: 4.weeks.ago, end_date: Date.current)
@@ -398,6 +317,10 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:weekly_event) { 'i_search_total' }
let(:redis_event) { described_class.send(:event_for, weekly_event) }
+ let(:week_one) { "{#{described_class::REDIS_SLOT}}_i_search_total-2020-52" }
+ let(:week_two) { "{#{described_class::REDIS_SLOT}}_i_search_total-2020-53" }
+ let(:week_three) { "{#{described_class::REDIS_SLOT}}_i_search_total-2021-01" }
+ let(:week_four) { "{#{described_class::REDIS_SLOT}}_i_search_total-2021-02" }
subject(:weekly_redis_keys) { described_class.send(:weekly_redis_keys, events: [redis_event], start_date: DateTime.parse(start_date), end_date: DateTime.parse(end_date)) }
@@ -406,13 +329,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'2020-12-21' | '2020-12-20' | []
'2020-12-21' | '2020-11-21' | []
'2021-01-01' | '2020-12-28' | []
- '2020-12-21' | '2020-12-28' | ['i_{search}_total-2020-52']
- '2020-12-21' | '2021-01-01' | ['i_{search}_total-2020-52']
- '2020-12-27' | '2021-01-01' | ['i_{search}_total-2020-52']
- '2020-12-26' | '2021-01-04' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53']
- '2020-12-26' | '2021-01-11' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53', 'i_{search}_total-2021-01']
- '2020-12-26' | '2021-01-17' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53', 'i_{search}_total-2021-01']
- '2020-12-26' | '2021-01-18' | ['i_{search}_total-2020-52', 'i_{search}_total-2020-53', 'i_{search}_total-2021-01', 'i_{search}_total-2021-02']
+ '2020-12-21' | '2020-12-28' | lazy { [week_one] }
+ '2020-12-21' | '2021-01-01' | lazy { [week_one] }
+ '2020-12-27' | '2021-01-01' | lazy { [week_one] }
+ '2020-12-26' | '2021-01-04' | lazy { [week_one, week_two] }
+ '2020-12-26' | '2021-01-11' | lazy { [week_one, week_two, week_three] }
+ '2020-12-26' | '2021-01-17' | lazy { [week_one, week_two, week_three] }
+ '2020-12-26' | '2021-01-18' | lazy { [week_one, week_two, week_three, week_four] }
end
with_them do
@@ -435,9 +358,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: 'event_name_1', redis_slot: 'event', category: 'category1', aggregation: "weekly" },
- { name: 'event_name_2', redis_slot: 'event', category: 'category1', aggregation: "weekly" },
- { name: 'event_name_3', redis_slot: 'event', category: 'category1', aggregation: "weekly" }
+ { name: 'event_name_1', aggregation: "weekly" },
+ { name: 'event_name_2', aggregation: "weekly" },
+ { name: 'event_name_3', aggregation: "weekly" }
].map(&:with_indifferent_access)
end
@@ -476,11 +399,11 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:time_range) { { start_date: 7.days.ago, end_date: DateTime.current } }
let(:known_events) do
[
- { name: 'event1_slot', redis_slot: "slot", category: 'category1', aggregation: "weekly" },
- { name: 'event2_slot', redis_slot: "slot", category: 'category2', aggregation: "weekly" },
- { name: 'event3_slot', redis_slot: "slot", category: 'category3', aggregation: "weekly" },
- { name: 'event5_slot', redis_slot: "slot", category: 'category4', aggregation: "daily" },
- { name: 'event4', category: 'category2', aggregation: "weekly" }
+ { name: 'event1_slot', aggregation: "weekly" },
+ { name: 'event2_slot', aggregation: "weekly" },
+ { name: 'event3_slot', aggregation: "weekly" },
+ { name: 'event5_slot', aggregation: "daily" },
+ { name: 'event4', aggregation: "weekly" }
].map(&:with_indifferent_access)
end
@@ -510,11 +433,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect(described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event2_slot event3_slot]))).to eq 3
end
- it 'validates and raise exception if events has mismatched slot or aggregation', :aggregate_failure do
- expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event1_slot event4])) }.to raise_error described_class::SlotMismatch
- expect { described_class.calculate_events_union(**time_range.merge(event_names: %w[event5_slot event3_slot])) }.to raise_error described_class::AggregationMismatch
- end
-
it 'returns 0 if there are no keys for given events' do
expect(Gitlab::Redis::HLL).not_to receive(:count)
expect(described_class.calculate_events_union(event_names: %w[event1_slot event2_slot event3_slot], start_date: Date.current, end_date: 4.weeks.ago)).to eq(-1)
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 33e0d446fca..383938b0324 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
@@ -306,7 +306,7 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
described_class.track_issue_assignee_changed_action(author: user3, project: project)
end
- events = Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(described_class::ISSUE_CATEGORY)
+ events = [described_class::ISSUE_TITLE_CHANGED, described_class::ISSUE_DESCRIPTION_CHANGED, described_class::ISSUE_ASSIGNEE_CHANGED]
today_count = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: time, end_date: time)
week_count = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: events, start_date: time - 5.days, end_date: 1.day.since(time))
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 42aa84c2c3e..e41da6d9ea2 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
@@ -69,7 +69,6 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:project) { target_project }
let(:namespace) { project.namespace.reload }
let(:user) { project.creator }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:label) { 'redis_hll_counters.code_review.i_code_review_user_create_mr_monthly' }
let(:property) { described_class::MR_USER_CREATE_ACTION }
end
@@ -118,7 +117,6 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:project) { target_project }
let(:namespace) { project.namespace.reload }
let(:user) { project.creator }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:label) { 'redis_hll_counters.code_review.i_code_review_user_approve_mr_monthly' }
let(:property) { described_class::MR_APPROVE_ACTION }
end
diff --git a/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb b/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
deleted file mode 100644
index d1144dd0bc5..00000000000
--- a/spec/lib/gitlab/usage_data_counters/track_unique_events_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::UsageDataCounters::TrackUniqueEvents, :clean_gitlab_redis_shared_state do
- subject(:track_unique_events) { described_class }
-
- let(:time) { Time.zone.now }
-
- def track_event(params)
- track_unique_events.track_event(**params)
- end
-
- def count_unique(params)
- track_unique_events.count_unique_events(**params)
- end
-
- context 'tracking an event' do
- context 'when tracking successfully' do
- context 'when the application setting is enabled' do
- context 'when the target and the action is valid' do
- before do
- stub_application_setting(usage_ping_enabled: true)
- end
-
- it 'tracks and counts the events as expected' do
- project = Event::TARGET_TYPES[:project]
- design = Event::TARGET_TYPES[:design]
- wiki = Event::TARGET_TYPES[:wiki]
-
- expect(track_event(event_action: :pushed, event_target: project, author_id: 1)).to be_truthy
- expect(track_event(event_action: :pushed, event_target: project, author_id: 1)).to be_truthy
- expect(track_event(event_action: :pushed, event_target: project, author_id: 2)).to be_truthy
- expect(track_event(event_action: :pushed, event_target: project, author_id: 3)).to be_truthy
- expect(track_event(event_action: :pushed, event_target: project, author_id: 4, time: time - 3.days)).to be_truthy
-
- expect(track_event(event_action: :destroyed, event_target: design, author_id: 3)).to be_truthy
- expect(track_event(event_action: :created, event_target: design, author_id: 4)).to be_truthy
- expect(track_event(event_action: :updated, event_target: design, author_id: 5)).to be_truthy
-
- expect(track_event(event_action: :destroyed, event_target: wiki, author_id: 5)).to be_truthy
- expect(track_event(event_action: :created, event_target: wiki, author_id: 3)).to be_truthy
- expect(track_event(event_action: :updated, event_target: wiki, author_id: 4)).to be_truthy
-
- expect(count_unique(event_action: described_class::PUSH_ACTION, date_from: time, date_to: Date.today)).to eq(3)
- expect(count_unique(event_action: described_class::PUSH_ACTION, date_from: time - 5.days, date_to: Date.tomorrow)).to eq(4)
- expect(count_unique(event_action: described_class::DESIGN_ACTION, date_from: time - 5.days, date_to: Date.today)).to eq(3)
- expect(count_unique(event_action: described_class::WIKI_ACTION, date_from: time - 5.days, date_to: Date.today)).to eq(3)
- expect(count_unique(event_action: described_class::PUSH_ACTION, date_from: time - 5.days, date_to: time - 2.days)).to eq(1)
- end
- end
- end
- end
-
- context 'when tracking unsuccessfully' do
- using RSpec::Parameterized::TableSyntax
-
- where(:target, :action) do
- Project | :invalid_action
- :invalid_target | :pushed
- Project | :created
- end
-
- with_them do
- it 'returns the expected values' do
- expect(track_event(event_action: action, event_target: target, author_id: 2)).to be_nil
- expect(count_unique(event_action: described_class::PUSH_ACTION, date_from: time, date_to: Date.today)).to eq(0)
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 5325ef5b5dd..d529319e6e9 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -529,8 +529,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
expect(count_data[:projects_prometheus_active]).to eq(1)
expect(count_data[:projects_jenkins_active]).to eq(1)
expect(count_data[:projects_jira_active]).to eq(4)
- expect(count_data[:projects_jira_server_active]).to eq(2)
- expect(count_data[:projects_jira_cloud_active]).to eq(2)
expect(count_data[:jira_imports_projects_count]).to eq(2)
expect(count_data[:jira_imports_total_imported_count]).to eq(3)
expect(count_data[:jira_imports_total_imported_issues_count]).to eq(13)
@@ -614,14 +612,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
it 'raises an error' do
expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
-
- context 'when metric calls find_in_batches' do
- let(:metric_method) { :find_in_batches }
-
- it 'raises an error for jira_usage' do
- expect { described_class.jira_usage }.to raise_error(ActiveRecord::StatementInvalid)
- end
- end
end
context 'with should_raise_for_dev? false' do
@@ -630,14 +620,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
it 'does not raise an error' do
expect { subject }.not_to raise_error
end
-
- context 'when metric calls find_in_batches' do
- let(:metric_method) { :find_in_batches }
-
- it 'does not raise an error for jira_usage' do
- expect { described_class.jira_usage }.not_to raise_error
- end
- end
end
end
@@ -1044,16 +1026,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
let(:time) { Time.current }
before do
- counter = Gitlab::UsageDataCounters::TrackUniqueEvents
- merge_request = Event::TARGET_TYPES[:merge_request]
- design = Event::TARGET_TYPES[:design]
-
- counter.track_event(event_action: :commented, event_target: merge_request, author_id: 1, time: time)
- counter.track_event(event_action: :opened, event_target: merge_request, author_id: 1, time: time)
- counter.track_event(event_action: :merged, event_target: merge_request, author_id: 2, time: time)
- counter.track_event(event_action: :closed, event_target: merge_request, author_id: 3, time: time)
- counter.track_event(event_action: :opened, event_target: merge_request, author_id: 4, time: time - 3.days)
- counter.track_event(event_action: :created, event_target: design, author_id: 5, time: time)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:merge_request_action, values: 1, time: time)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:merge_request_action, values: 1, time: time)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:merge_request_action, values: 2, time: time)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:merge_request_action, values: 3, time: time)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:merge_request_action, values: 4, time: time - 3.days)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:design_action, values: 5, time: time)
end
it 'returns the distinct count of users using merge requests (via events table) within the specified time period' do
@@ -1069,42 +1047,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
end
end
- describe '#action_monthly_active_users', :clean_gitlab_redis_shared_state do
- let(:time_period) { { created_at: 2.days.ago..time } }
- let(:time) { Time.zone.now }
- let(:user1) { build(:user, id: 1) }
- let(:user2) { build(:user, id: 2) }
- let(:user3) { build(:user, id: 3) }
- let(:user4) { build(:user, id: 4) }
- let(:project) { build(:project) }
-
- before do
- counter = Gitlab::UsageDataCounters::EditorUniqueCounter
-
- counter.track_web_ide_edit_action(author: user1, project: project)
- counter.track_web_ide_edit_action(author: user1, project: project)
- counter.track_sfe_edit_action(author: user1, project: project)
- counter.track_snippet_editor_edit_action(author: user1, project: project)
- counter.track_snippet_editor_edit_action(author: user1, time: time - 3.days, project: project)
-
- counter.track_web_ide_edit_action(author: user2, project: project)
- counter.track_sfe_edit_action(author: user2, project: project)
-
- counter.track_web_ide_edit_action(author: user3, time: time - 3.days, project: project)
- counter.track_snippet_editor_edit_action(author: user3, project: project)
- end
-
- it 'returns the distinct count of user actions within the specified time period' do
- expect(described_class.action_monthly_active_users(time_period)).to eq(
- {
- action_monthly_active_users_web_ide_edit: 2,
- action_monthly_active_users_sfe_edit: 2,
- action_monthly_active_users_snippet_editor_edit: 2
- }
- )
- end
- end
-
describe '.service_desk_counts' do
subject { described_class.send(:service_desk_counts) }
diff --git a/spec/lib/gitlab/utils/error_message_spec.rb b/spec/lib/gitlab/utils/error_message_spec.rb
new file mode 100644
index 00000000000..2c2d16656e8
--- /dev/null
+++ b/spec/lib/gitlab/utils/error_message_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Utils::ErrorMessage, feature_category: :error_tracking do
+ let(:klass) do
+ Class.new do
+ include Gitlab::Utils::ErrorMessage
+ end
+ end
+
+ subject(:object) { klass.new }
+
+ describe 'error message' do
+ subject { object.to_user_facing(string) }
+
+ let(:string) { 'Error Message' }
+
+ it "returns input prefixed with UF:" do
+ is_expected.to eq 'UF: Error Message'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
index 71f2502b91c..27bfe181ef6 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -8,7 +8,7 @@ RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
end
-RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :not_owned do
+RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
let(:klass) do
strong_memoize_class = described_class
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/lib/gitlab/utils/uniquify_spec.rb
index 9b79e4d4154..df02fbe8c82 100644
--- a/spec/models/concerns/uniquify_spec.rb
+++ b/spec/lib/gitlab/utils/uniquify_spec.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
-RSpec.describe Uniquify do
- let(:uniquify) { described_class.new }
+RSpec.describe Gitlab::Utils::Uniquify, feature_category: :shared do
+ subject(:uniquify) { described_class.new }
describe "#string" do
it 'returns the given string if it does not exist' do
- result = uniquify.string('test_string') { |s| false }
+ result = uniquify.string('test_string') { |_s| false }
expect(result).to eq('test_string')
end
@@ -34,7 +34,7 @@ RSpec.describe Uniquify do
end
it 'allows passing in a base function that defines the location of the counter' do
- result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
+ result = uniquify.string(->(counter) { "test_#{counter}_string" }) do |s|
s == 'test__string'
end
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
index 2925ceef256..586ee04a835 100644
--- a/spec/lib/gitlab/utils/usage_data_spec.rb
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -487,12 +487,12 @@ RSpec.describe Gitlab::Utils::UsageData do
end
context 'when Redis HLL raises any error' do
- subject { described_class.redis_usage_data { raise Gitlab::UsageDataCounters::HLLRedisCounter::CategoryMismatch } }
+ subject { described_class.redis_usage_data { raise Gitlab::UsageDataCounters::HLLRedisCounter::EventError } }
let(:fallback) { 15 }
let(:failing_class) { nil }
- it_behaves_like 'failing hardening method', Gitlab::UsageDataCounters::HLLRedisCounter::CategoryMismatch
+ it_behaves_like 'failing hardening method', Gitlab::UsageDataCounters::HLLRedisCounter::EventError
end
it 'returns the evaluated block when given' do
diff --git a/spec/lib/gitlab/utils/username_and_email_generator_spec.rb b/spec/lib/gitlab/utils/username_and_email_generator_spec.rb
new file mode 100644
index 00000000000..45df8f08055
--- /dev/null
+++ b/spec/lib/gitlab/utils/username_and_email_generator_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Utils::UsernameAndEmailGenerator, feature_category: :system_access do
+ let(:username_prefix) { 'username_prefix' }
+ let(:email_domain) { 'example.com' }
+
+ subject { described_class.new(username_prefix: username_prefix, email_domain: email_domain) }
+
+ describe 'email domain' do
+ it 'defaults to `Gitlab.config.gitlab.host`' do
+ expect(described_class.new(username_prefix: username_prefix).email).to end_with("@#{Gitlab.config.gitlab.host}")
+ end
+
+ context 'when specified' do
+ it 'uses the specified email domain' do
+ expect(subject.email).to end_with("@#{email_domain}")
+ end
+ end
+ end
+
+ include_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator'
+end
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 2a81142ea44..412fcb9b6b8 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
require 'fog/core'
-RSpec.describe ObjectStorage::Config do
+RSpec.describe ObjectStorage::Config, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
let(:region) { 'us-east-1' }
@@ -130,6 +130,11 @@ RSpec.describe ObjectStorage::Config do
it { expect(subject.provider).to eq('AWS') }
it { expect(subject.aws?).to be true }
it { expect(subject.google?).to be false }
+ it { expect(subject.credentials).to eq(credentials) }
+
+ context 'with FIPS enabled', :fips_mode do
+ it { expect(subject.credentials).to eq(credentials.merge(disable_content_md5_validation: true)) }
+ end
end
context 'with Google credentials' do
diff --git a/spec/lib/security/weak_passwords_spec.rb b/spec/lib/security/weak_passwords_spec.rb
index afa9448e746..14bab5ee6ec 100644
--- a/spec/lib/security/weak_passwords_spec.rb
+++ b/spec/lib/security/weak_passwords_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Security::WeakPasswords, feature_category: :authentication_and_authorization do
+RSpec.describe Security::WeakPasswords, feature_category: :system_access do
describe "#weak_for_user?" do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/sidebars/concerns/super_sidebar_panel_spec.rb b/spec/lib/sidebars/concerns/super_sidebar_panel_spec.rb
new file mode 100644
index 00000000000..f33cb4ab7f6
--- /dev/null
+++ b/spec/lib/sidebars/concerns/super_sidebar_panel_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Sidebars::Concerns::SuperSidebarPanel, feature_category: :navigation do
+ let(:menu_class_foo) { Class.new(Sidebars::Menu) }
+ let(:menu_foo) { menu_class_foo.new({}) }
+
+ let(:menu_class_bar) do
+ Class.new(Sidebars::Menu) do
+ def title
+ "Bar"
+ end
+
+ def pick_into_super_sidebar?
+ true
+ end
+ end
+ end
+
+ let(:menu_bar) { menu_class_bar.new({}) }
+
+ subject do
+ Class.new(Sidebars::Panel) do
+ include Sidebars::Concerns::SuperSidebarPanel
+ end.new({})
+ end
+
+ before do
+ allow(menu_foo).to receive(:render?).and_return(true)
+ allow(menu_bar).to receive(:render?).and_return(true)
+ end
+
+ describe '#pick_from_old_menus' do
+ it 'removes items with #pick_into_super_sidebar? from a list and adds them to the panel menus' do
+ old_menus = [menu_foo, menu_bar]
+
+ subject.pick_from_old_menus(old_menus)
+
+ expect(old_menus).to include(menu_foo)
+ expect(subject.renderable_menus).not_to include(menu_foo)
+
+ expect(old_menus).not_to include(menu_bar)
+ expect(subject.renderable_menus).to include(menu_bar)
+ end
+ end
+
+ describe '#transform_old_menus' do
+ let(:uncategorized_menu) { ::Sidebars::UncategorizedMenu.new({}) }
+
+ let(:menu_item) do
+ Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' },
+ super_sidebar_parent: menu_class_foo)
+ end
+
+ let(:nil_menu_item) { Sidebars::NilMenuItem.new(item_id: :nil_item) }
+ let(:existing_item) do
+ Sidebars::MenuItem.new(
+ item_id: :exists,
+ title: 'Existing item',
+ link: 'foo2',
+ active_routes: { controller: 'foo2' }
+ )
+ end
+
+ let(:current_menus) { [menu_foo, uncategorized_menu] }
+
+ before do
+ allow(menu_bar).to receive(:serialize_as_menu_item_args).and_return(nil)
+ menu_foo.add_item(existing_item)
+ end
+
+ context 'for Menus with Menu Items' do
+ before do
+ menu_bar.add_item(menu_item)
+ menu_bar.add_item(nil_menu_item)
+ end
+
+ it 'adds Menu Items to defined super_sidebar_parent' do
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([existing_item, menu_item])
+ expect(uncategorized_menu.renderable_items).to eq([])
+ end
+
+ it 'adds Menu Items to defined super_sidebar_parent, before super_sidebar_before' do
+ allow(menu_item).to receive(:super_sidebar_before).and_return(:exists)
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([menu_item, existing_item])
+ expect(uncategorized_menu.renderable_items).to eq([])
+ end
+
+ it 'considers Menu Items uncategorized if super_sidebar_parent is nil' do
+ allow(menu_item).to receive(:super_sidebar_parent).and_return(nil)
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([existing_item])
+ expect(uncategorized_menu.renderable_items).to eq([menu_item])
+ end
+
+ it 'considers Menu Items uncategorized if super_sidebar_parent cannot be found' do
+ allow(menu_item).to receive(:super_sidebar_parent).and_return(menu_class_bar)
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([existing_item])
+ expect(uncategorized_menu.renderable_items).to eq([menu_item])
+ end
+
+ it 'considers Menu Items deleted if super_sidebar_parent is Sidebars::NilMenuItem' do
+ allow(menu_item).to receive(:super_sidebar_parent).and_return(::Sidebars::NilMenuItem)
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([existing_item])
+ expect(uncategorized_menu.renderable_items).to eq([])
+ end
+ end
+
+ it 'converts "solo" top-level Menu entry to Menu Item' do
+ allow(Sidebars::MenuItem).to receive(:new).and_return(menu_item)
+ allow(menu_bar).to receive(:serialize_as_menu_item_args).and_return({})
+
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([existing_item, menu_item])
+ expect(uncategorized_menu.renderable_items).to eq([])
+ end
+
+ it 'drops "solo" top-level Menu entries, if they serialize to nil' do
+ allow(Sidebars::MenuItem).to receive(:new).and_return(menu_item)
+ allow(menu_bar).to receive(:serialize_as_menu_item_args).and_return(nil)
+
+ subject.transform_old_menus(current_menus, menu_bar)
+
+ expect(menu_foo.renderable_items).to eq([existing_item])
+ expect(uncategorized_menu.renderable_items).to eq([])
+ end
+ end
+end
diff --git a/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb b/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb
index 1b27db53b6f..4a0301e2f2d 100644
--- a/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/group_information_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Groups::Menus::GroupInformationMenu do
+RSpec.describe Sidebars::Groups::Menus::GroupInformationMenu, feature_category: :navigation do
let_it_be(:owner) { create(:user) }
let_it_be(:root_group) do
build(:group, :private).tap do |g|
@@ -14,6 +14,10 @@ RSpec.describe Sidebars::Groups::Menus::GroupInformationMenu do
let(:user) { owner }
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ it_behaves_like 'not serializable as super_sidebar_menu_args' do
+ let(:menu) { described_class.new(context) }
+ end
+
describe '#title' do
subject { described_class.new(context).title }
diff --git a/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb
deleted file mode 100644
index a79e5182f45..00000000000
--- a/spec/lib/sidebars/groups/menus/invite_team_members_menu_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Groups::Menus::InviteTeamMembersMenu do
- let_it_be(:owner) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:group) do
- build(:group).tap do |g|
- g.add_owner(owner)
- end
- end
-
- let(:context) { Sidebars::Groups::Context.new(current_user: owner, container: group) }
-
- subject(:invite_menu) { described_class.new(context) }
-
- context 'when the group is viewed by an owner of the group' do
- describe '#render?' do
- it 'renders the Invite team members link' do
- expect(invite_menu.render?).to eq(true)
- end
-
- context 'when the group already has at least 2 members' do
- before do
- group.add_guest(guest)
- end
-
- it 'does not render the link' do
- expect(invite_menu.render?).to eq(false)
- end
- end
- end
-
- describe '#title' do
- it 'displays the correct Invite team members text for the link in the side nav' do
- expect(invite_menu.title).to eq('Invite members')
- end
- end
- end
-
- context 'when the group is viewed by a guest user without admin permissions' do
- let(:context) { Sidebars::Groups::Context.new(current_user: guest, container: group) }
-
- before do
- group.add_guest(guest)
- end
-
- describe '#render?' do
- it 'does not render the link' do
- expect(subject.render?).to eq(false)
- end
- end
- end
-end
diff --git a/spec/lib/sidebars/groups/menus/issues_menu_spec.rb b/spec/lib/sidebars/groups/menus/issues_menu_spec.rb
index 3d55eb3af40..ceeda4a7ac3 100644
--- a/spec/lib/sidebars/groups/menus/issues_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/issues_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Groups::Menus::IssuesMenu do
+RSpec.describe Sidebars::Groups::Menus::IssuesMenu, feature_category: :navigation do
let_it_be(:owner) { create(:user) }
let_it_be(:group) do
build(:group, :private).tap do |g|
@@ -51,4 +51,17 @@ RSpec.describe Sidebars::Groups::Menus::IssuesMenu do
it_behaves_like 'pill_count formatted results' do
let(:count_service) { ::Groups::OpenIssuesCountService }
end
+
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:extra_attrs) do
+ {
+ item_id: :group_issue_list,
+ active_routes: { path: 'groups#issues' },
+ sprite_icon: 'issues',
+ pill_count: menu.pill_count,
+ has_pill: menu.has_pill?,
+ super_sidebar_parent: ::Sidebars::StaticMenu
+ }
+ end
+ end
end
diff --git a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb
index 5bf8be9d6e5..8eb9a22e3e1 100644
--- a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Groups::Menus::KubernetesMenu, :request_store do
+RSpec.describe Sidebars::Groups::Menus::KubernetesMenu, :request_store, feature_category: :navigation do
let_it_be(:owner) { create(:user) }
let_it_be(:group) do
build(:group, :private).tap do |g|
@@ -14,6 +14,15 @@ RSpec.describe Sidebars::Groups::Menus::KubernetesMenu, :request_store do
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
let(:menu) { described_class.new(context) }
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:extra_attrs) do
+ {
+ super_sidebar_parent: Sidebars::Groups::SuperSidebarMenus::OperationsMenu,
+ item_id: :group_kubernetes_clusters
+ }
+ end
+ end
+
describe '#render?' do
context 'when user can read clusters' do
it 'returns true' do
diff --git a/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb b/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
index 3aceff29d6d..72f85f7930a 100644
--- a/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Groups::Menus::MergeRequestsMenu do
+RSpec.describe Sidebars::Groups::Menus::MergeRequestsMenu, feature_category: :navigation do
let_it_be(:owner) { create(:user) }
let_it_be(:group) do
build(:group, :private).tap do |g|
@@ -33,4 +33,16 @@ RSpec.describe Sidebars::Groups::Menus::MergeRequestsMenu do
it_behaves_like 'pill_count formatted results' do
let(:count_service) { ::Groups::MergeRequestsCountService }
end
+
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:extra_attrs) do
+ {
+ item_id: :group_merge_request_list,
+ sprite_icon: 'git-merge',
+ pill_count: menu.pill_count,
+ has_pill: menu.has_pill?,
+ super_sidebar_parent: ::Sidebars::StaticMenu
+ }
+ end
+ end
end
diff --git a/spec/lib/sidebars/groups/menus/observability_menu_spec.rb b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb
index 5b993cd6f28..20af8ea00be 100644
--- a/spec/lib/sidebars/groups/menus/observability_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/observability_menu_spec.rb
@@ -20,26 +20,74 @@ RSpec.describe Sidebars::Groups::Menus::ObservabilityMenu do
allow(menu).to receive(:can?).and_call_original
end
- context 'when observability is enabled' do
+ context 'when observability#explore is allowed' do
before do
- allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(true)
+ allow(Gitlab::Observability).to receive(:allowed_for_action?).with(user, group, :explore).and_return(true)
end
it 'returns true' do
expect(menu.render?).to eq true
- expect(Gitlab::Observability).to have_received(:observability_enabled?).with(user, group)
+ expect(Gitlab::Observability).to have_received(:allowed_for_action?).with(user, group, :explore)
end
end
- context 'when observability is disabled' do
+ context 'when observability#explore is not allowed' do
before do
- allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(false)
+ allow(Gitlab::Observability).to receive(:allowed_for_action?).with(user, group, :explore).and_return(false)
end
it 'returns false' do
expect(menu.render?).to eq false
- expect(Gitlab::Observability).to have_received(:observability_enabled?).with(user, group)
+ expect(Gitlab::Observability).to have_received(:allowed_for_action?).with(user, group, :explore)
end
end
end
+
+ describe "Menu items" do
+ before do
+ allow(Gitlab::Observability).to receive(:allowed_for_action?).and_return(false)
+ end
+
+ subject { find_menu(menu, item_id) }
+
+ shared_examples 'observability menu entry' do
+ context 'when action is allowed' do
+ before do
+ allow(Gitlab::Observability).to receive(:allowed_for_action?).with(user, group, item_id).and_return(true)
+ end
+
+ it 'the menu item is added to list of menu items' do
+ is_expected.not_to be_nil
+ end
+ end
+
+ context 'when action is not allowed' do
+ before do
+ allow(Gitlab::Observability).to receive(:allowed_for_action?).with(user, group, item_id).and_return(false)
+ end
+
+ it 'the menu item is added to list of menu items' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe 'Explore' do
+ it_behaves_like 'observability menu entry' do
+ let(:item_id) { :explore }
+ end
+ end
+
+ describe 'Datasources' do
+ it_behaves_like 'observability menu entry' do
+ let(:item_id) { :datasources }
+ end
+ end
+ end
+
+ private
+
+ def find_menu(menu, item)
+ menu.renderable_items.find { |i| i.item_id == item }
+ 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 ce368ad5bd6..382ee07e458 100644
--- a/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
+RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu, feature_category: :navigation do
let_it_be(:owner) { create(:user) }
let_it_be_with_reload(:group) do
build(:group, :private).tap do |g|
@@ -16,6 +16,8 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
let(:menu) { described_class.new(context) }
+ it_behaves_like 'not serializable as super_sidebar_menu_args'
+
describe '#render?' do
context 'when menu has menu items to show' do
it 'returns true' do
diff --git a/spec/lib/sidebars/groups/menus/scope_menu_spec.rb b/spec/lib/sidebars/groups/menus/scope_menu_spec.rb
index 4b77a09117a..d3aceaf422b 100644
--- a/spec/lib/sidebars/groups/menus/scope_menu_spec.rb
+++ b/spec/lib/sidebars/groups/menus/scope_menu_spec.rb
@@ -2,14 +2,26 @@
require 'spec_helper'
-RSpec.describe Sidebars::Groups::Menus::ScopeMenu do
+RSpec.describe Sidebars::Groups::Menus::ScopeMenu, feature_category: :navigation do
let(:group) { build(:group) }
let(:user) { group.owner }
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
describe '#extra_nav_link_html_options' do
- subject { described_class.new(context).extra_nav_link_html_options }
+ subject { menu.extra_nav_link_html_options }
specify { is_expected.to match(hash_including(class: 'context-header has-tooltip', title: context.group.name)) }
end
+
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:extra_attrs) do
+ {
+ sprite_icon: 'group',
+ super_sidebar_parent: ::Sidebars::StaticMenu,
+ title: _('Group overview'),
+ item_id: :group_overview
+ }
+ end
+ end
end
diff --git a/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb b/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb
new file mode 100644
index 00000000000..beaf3875f1c
--- /dev/null
+++ b/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::SuperSidebarPanel, feature_category: :navigation do
+ let_it_be(:group) { create(:group) }
+
+ let(:user) { group.first_owner }
+
+ let(:context) do
+ double("Stubbed context", current_user: user, container: group, group: group).as_null_object # rubocop:disable RSpec/VerifiedDoubles
+ end
+
+ subject { described_class.new(context) }
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq(
+ {
+ title: group.name,
+ avatar: group.avatar_url,
+ id: group.id
+ })
+ end
+
+ describe '#renderable_menus' do
+ let(:category_menu) do
+ [
+ Sidebars::StaticMenu,
+ Sidebars::Groups::SuperSidebarMenus::PlanMenu,
+ Sidebars::Groups::Menus::CiCdMenu,
+ (Sidebars::Groups::Menus::SecurityComplianceMenu if Gitlab.ee?),
+ Sidebars::Groups::SuperSidebarMenus::OperationsMenu,
+ Sidebars::Groups::Menus::ObservabilityMenu,
+ (Sidebars::Groups::Menus::AnalyticsMenu if Gitlab.ee?),
+ Sidebars::UncategorizedMenu,
+ Sidebars::Groups::Menus::SettingsMenu
+ ].compact
+ end
+
+ it "is exposed as a renderable menu" do
+ expect(subject.instance_variable_get(:@menus).map(&:class)).to eq(category_menu)
+ end
+ end
+end
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index 53a889c2db8..641f1c6e7e6 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -2,9 +2,10 @@
require 'spec_helper'
-RSpec.describe Sidebars::Menu do
+RSpec.describe Sidebars::Menu, feature_category: :navigation do
let(:menu) { described_class.new(context) }
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
let(:nil_menu_item) { Sidebars::NilMenuItem.new(item_id: :foo) }
describe '#all_active_routes' do
@@ -21,6 +22,82 @@ RSpec.describe Sidebars::Menu do
end
end
+ describe '#serialize_for_super_sidebar' do
+ before do
+ allow(menu).to receive(:title).and_return('Title')
+ allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
+ end
+
+ it 'returns a tree-like structure of itself and all menu items' do
+ menu.add_item(Sidebars::MenuItem.new(title: 'Is active', link: 'foo2', active_routes: { controller: 'fooc' }))
+ menu.add_item(Sidebars::MenuItem.new(
+ title: 'Not active',
+ link: 'foo3',
+ active_routes: { controller: 'barc' },
+ has_pill: true,
+ pill_count: 10
+ ))
+ menu.add_item(nil_menu_item)
+
+ allow(context).to receive(:route_is_active).and_return(->(x) { x[:controller] == 'fooc' })
+
+ expect(menu.serialize_for_super_sidebar).to eq(
+ {
+ title: "Title",
+ icon: nil,
+ link: "foo2",
+ is_active: true,
+ pill_count: nil,
+ items: [
+ {
+ title: "Is active",
+ icon: nil,
+ link: "foo2",
+ is_active: true,
+ pill_count: nil
+ },
+ {
+ title: "Not active",
+ icon: nil,
+ link: "foo3",
+ is_active: false,
+ pill_count: 10
+ }
+ ]
+ })
+ end
+
+ it 'returns pill data if defined' do
+ allow(menu).to receive(:has_pill?).and_return(true)
+ allow(menu).to receive(:pill_count).and_return('foo')
+ expect(menu.serialize_for_super_sidebar).to eq(
+ {
+ title: "Title",
+ icon: nil,
+ link: nil,
+ is_active: false,
+ pill_count: 'foo',
+ items: []
+ })
+ end
+ end
+
+ describe '#serialize_as_menu_item_args' do
+ it 'returns hash of title, link, active_routes, container_html_options' do
+ allow(menu).to receive(:title).and_return('Title')
+ allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
+ allow(menu).to receive(:container_html_options).and_return({ class: 'foo' })
+ allow(menu).to receive(:link).and_return('/link')
+
+ expect(menu.serialize_as_menu_item_args).to eq({
+ title: 'Title',
+ link: '/link',
+ active_routes: { path: 'foo' },
+ container_html_options: { class: 'foo' }
+ })
+ end
+ end
+
describe '#render?' do
context 'when the menus has no items' do
it 'returns false' do
diff --git a/spec/lib/sidebars/panel_spec.rb b/spec/lib/sidebars/panel_spec.rb
index b70a79361d0..2c1b9c73595 100644
--- a/spec/lib/sidebars/panel_spec.rb
+++ b/spec/lib/sidebars/panel_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe Sidebars::Panel do
+RSpec.describe Sidebars::Panel, feature_category: :navigation do
let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
let(:panel) { Sidebars::Panel.new(context) }
let(:menu1) { Sidebars::Menu.new(context) }
let(:menu2) { Sidebars::Menu.new(context) }
+ let(:menu3) { Sidebars::Menu.new(context) }
describe '#renderable_menus' do
it 'returns only renderable menus' do
@@ -20,6 +21,31 @@ RSpec.describe Sidebars::Panel do
end
end
+ describe '#super_sidebar_menu_items' do
+ it "serializes every renderable menu and returns a flattened result" do
+ panel.add_menu(menu1)
+ panel.add_menu(menu2)
+ panel.add_menu(menu3)
+
+ allow(menu1).to receive(:render?).and_return(true)
+ allow(menu1).to receive(:serialize_for_super_sidebar).and_return("foo")
+
+ allow(menu2).to receive(:render?).and_return(false)
+ allow(menu2).to receive(:serialize_for_super_sidebar).and_return("i-should-not-appear-in-results")
+
+ allow(menu3).to receive(:render?).and_return(true)
+ allow(menu3).to receive(:serialize_for_super_sidebar).and_return(%w[bar baz])
+
+ expect(panel.super_sidebar_menu_items).to eq(%w[foo bar baz])
+ end
+ end
+
+ describe '#super_sidebar_context_header' do
+ it 'raises `NotImplementedError`' do
+ expect { panel.super_sidebar_context_header }.to raise_error(NotImplementedError)
+ end
+ end
+
describe '#has_renderable_menus?' do
it 'returns false when no renderable menus' do
expect(panel.has_renderable_menus?).to be false
diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
index ce971915174..5065c261cf8 100644
--- a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb
@@ -2,12 +2,16 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
+RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu, feature_category: :navigation do
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) }
+ it_behaves_like 'not serializable as super_sidebar_menu_args' do
+ let(:menu) { described_class.new(context) }
+ end
+
describe '#render?' do
subject { described_class.new(context) }
diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
index 116948b7cb0..7f0e02892e4 100644
--- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
@@ -2,11 +2,15 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
+RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu, feature_category: :navigation do
let(:project) { build(:project) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_cluster_hint: false) }
+ it_behaves_like 'not serializable as super_sidebar_menu_args' do
+ let(:menu) { described_class.new(context) }
+ end
+
describe '#render?' do
subject { described_class.new(context) }
@@ -103,6 +107,22 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
let(:item_id) { :terraform }
it_behaves_like 'access rights checks'
+
+ context 'if terraform_state.enabled=true' do
+ before do
+ stub_config(terraform_state: { enabled: true })
+ end
+
+ it_behaves_like 'access rights checks'
+ end
+
+ context 'if terraform_state.enabled=false' do
+ before do
+ stub_config(terraform_state: { enabled: false })
+ end
+
+ it { is_expected.to be_nil }
+ end
end
describe 'Google Cloud' do
@@ -141,6 +161,56 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it_behaves_like 'access rights checks'
end
end
+
+ context 'when instance is not configured for Google OAuth2' do
+ before do
+ stub_feature_flags(incubation_5mp_google_cloud: true)
+ 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 { is_expected.to be_nil }
+ end
+ end
+
+ describe 'AWS' do
+ let(:item_id) { :aws }
+
+ it_behaves_like 'access rights checks'
+
+ context 'when feature flag is turned off globally' do
+ before do
+ stub_feature_flags(cloudseed_aws: false)
+ end
+
+ it { is_expected.to be_nil }
+
+ context 'when feature flag is enabled for specific project' do
+ before do
+ stub_feature_flags(cloudseed_aws: project)
+ end
+
+ it_behaves_like 'access rights checks'
+ end
+
+ context 'when feature flag is enabled for specific group' do
+ before do
+ stub_feature_flags(cloudseed_aws: project.group)
+ end
+
+ it_behaves_like 'access rights checks'
+ end
+
+ context 'when feature flag is enabled for specific project' do
+ before do
+ stub_feature_flags(cloudseed_aws: user)
+ end
+
+ it_behaves_like 'access rights checks'
+ end
+ end
end
end
end
diff --git a/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb b/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
deleted file mode 100644
index 9838aa8c3e3..00000000000
--- a/spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Menus::InviteTeamMembersMenu do
- let_it_be(:project) { create(:project) }
- let_it_be(:guest) { create(:user) }
-
- let(:context) { Sidebars::Projects::Context.new(current_user: owner, container: project) }
-
- subject(:invite_menu) { described_class.new(context) }
-
- context 'when the project is viewed by an owner of the group' do
- let(:owner) { project.first_owner }
-
- describe '#render?' do
- it 'renders the Invite team members link' do
- expect(invite_menu.render?).to eq(true)
- end
-
- context 'when the project already has at least 2 members' do
- before do
- project.add_guest(guest)
- end
-
- it 'does not render the link' do
- expect(invite_menu.render?).to eq(false)
- end
- end
- end
-
- describe '#title' do
- it 'displays the correct Invite team members text for the link in the side nav' do
- expect(invite_menu.title).to eq('Invite members')
- end
- end
- end
-
- context 'when the project is viewed by a guest user without admin permissions' do
- let(:context) { Sidebars::Projects::Context.new(current_user: guest, container: project) }
-
- before do
- project.add_guest(guest)
- end
-
- describe '#render?' do
- it 'does not render' do
- expect(invite_menu.render?).to eq(false)
- end
- end
- end
-end
diff --git a/spec/lib/sidebars/projects/menus/issues_menu_spec.rb b/spec/lib/sidebars/projects/menus/issues_menu_spec.rb
index 4c0016a77a1..c7ff846bc95 100644
--- a/spec/lib/sidebars/projects/menus/issues_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/issues_menu_spec.rb
@@ -2,13 +2,26 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::IssuesMenu do
+RSpec.describe Sidebars::Projects::Menus::IssuesMenu, feature_category: :navigation do
let(:project) { build(:project) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:menu) { subject }
+ let(:extra_attrs) do
+ {
+ item_id: :project_issue_list,
+ sprite_icon: 'issues',
+ pill_count: menu.pill_count,
+ has_pill: menu.has_pill?,
+ super_sidebar_parent: ::Sidebars::StaticMenu
+ }
+ end
+ end
+
describe '#render?' do
context 'when user can read issues' do
it 'returns true' do
diff --git a/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb b/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb
index 45c49500e46..a19df559b58 100644
--- a/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::MergeRequestsMenu do
+RSpec.describe Sidebars::Projects::Menus::MergeRequestsMenu, feature_category: :navigation do
let_it_be(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
@@ -10,6 +10,19 @@ RSpec.describe Sidebars::Projects::Menus::MergeRequestsMenu do
subject { described_class.new(context) }
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:menu) { subject }
+ let(:extra_attrs) do
+ {
+ item_id: :project_merge_request_list,
+ sprite_icon: 'git-merge',
+ pill_count: menu.pill_count,
+ has_pill: menu.has_pill?,
+ super_sidebar_parent: ::Sidebars::StaticMenu
+ }
+ end
+ end
+
describe '#render?' do
context 'when repository is not present' do
let(:project) { build(:project) }
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 b03269c424a..554bc763345 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
+RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu, feature_category: :navigation do
let_it_be(:project) { create(:project) }
let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
@@ -12,6 +12,10 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
subject { described_class.new(context) }
+ it_behaves_like 'not serializable as super_sidebar_menu_args' do
+ let(:menu) { subject }
+ end
+
describe '#render?' do
context 'when menu does not have any menu item to show' do
it 'returns false' do
diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
index 7ff06ac229e..7547f152b27 100644
--- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
@@ -2,12 +2,16 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
+RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu, feature_category: :navigation do
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+ it_behaves_like 'not serializable as super_sidebar_menu_args' do
+ let(:menu) { described_class.new(context) }
+ end
+
describe '#container_html_options' do
subject { described_class.new(context).container_html_options }
diff --git a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
index 40ca2107698..b0631aacdb9 100644
--- a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe Sidebars::Projects::Menus::RepositoryMenu, feature_category: :sou
end
end
- describe 'Contributors' do
+ describe 'Contributor statistics' do
let_it_be(:item_id) { :contributors }
context 'when analytics is disabled' do
diff --git a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
index 4e87f3b8ead..45464278880 100644
--- a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
@@ -2,11 +2,23 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::ScopeMenu do
+RSpec.describe Sidebars::Projects::Menus::ScopeMenu, feature_category: :navigation do
let(:project) { build(:project) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:menu) { described_class.new(context) }
+ let(:extra_attrs) do
+ {
+ title: _('Project overview'),
+ sprite_icon: 'project',
+ super_sidebar_parent: ::Sidebars::StaticMenu,
+ item_id: :project_overview
+ }
+ end
+ end
+
describe '#container_html_options' do
subject { described_class.new(context).container_html_options }
diff --git a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
index 41158bd58dc..697359b7941 100644
--- a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Sidebars::Projects::Menus::SecurityComplianceMenu do
end
context 'when user is authenticated' do
- context 'when the Security & Compliance is disabled' do
+ context 'when the Security and Compliance is disabled' do
before do
allow(Ability).to receive(:allowed?).with(user, :access_security_and_compliance, project).and_return(false)
end
@@ -28,7 +28,7 @@ RSpec.describe Sidebars::Projects::Menus::SecurityComplianceMenu do
it { is_expected.to be_falsey }
end
- context 'when the Security & Compliance is not disabled' do
+ context 'when the Security and Compliance is not disabled' do
it { is_expected.to be_truthy }
end
end
diff --git a/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb b/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
index 04b8c128e3d..c5fd407dae9 100644
--- a/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/snippets_menu_spec.rb
@@ -2,13 +2,24 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::SnippetsMenu do
+RSpec.describe Sidebars::Projects::Menus::SnippetsMenu, feature_category: :navigation do
let(:project) { build(:project) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:menu) { subject }
+ let(:extra_attrs) do
+ {
+ super_sidebar_parent: ::Sidebars::Projects::Menus::RepositoryMenu,
+ super_sidebar_before: :contributors,
+ item_id: :project_snippets
+ }
+ end
+ end
+
describe '#render?' do
context 'when user cannot access snippets' do
let(:user) { nil }
diff --git a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
index 362da3e7b50..64050e3e488 100644
--- a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Menus::WikiMenu do
+RSpec.describe Sidebars::Projects::Menus::WikiMenu, feature_category: :navigation do
let(:project) { build(:project) }
let(:user) { project.first_owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
@@ -28,4 +28,14 @@ RSpec.describe Sidebars::Projects::Menus::WikiMenu do
end
end
end
+
+ it_behaves_like 'serializable as super_sidebar_menu_args' do
+ let(:menu) { subject }
+ let(:extra_attrs) do
+ {
+ super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::PlanMenu,
+ item_id: :project_wiki
+ }
+ end
+ end
end
diff --git a/spec/lib/sidebars/projects/panel_spec.rb b/spec/lib/sidebars/projects/panel_spec.rb
index ff253eedd08..ec1df438cf1 100644
--- a/spec/lib/sidebars/projects/panel_spec.rb
+++ b/spec/lib/sidebars/projects/panel_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Sidebars::Projects::Panel do
+RSpec.describe Sidebars::Projects::Panel, feature_category: :navigation do
let_it_be(:project) { create(:project) }
let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) }
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb
new file mode 100644
index 00000000000..df3f7e6cdab
--- /dev/null
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::SuperSidebarMenus::OperationsMenu, feature_category: :navigation do
+ subject { described_class.new({}) }
+
+ it 'has title and sprite_icon' do
+ expect(subject.title).to eq(_("Operations"))
+ expect(subject.sprite_icon).to eq("deployments")
+ end
+end
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb
new file mode 100644
index 00000000000..3917d26f6f2
--- /dev/null
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::SuperSidebarMenus::PlanMenu, feature_category: :navigation do
+ subject { described_class.new({}) }
+
+ it 'has title and sprite_icon' do
+ expect(subject.title).to eq(_("Plan"))
+ expect(subject.sprite_icon).to eq("planning")
+ end
+end
diff --git a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb
new file mode 100644
index 00000000000..d6fc3fd8fe1
--- /dev/null
+++ b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::SuperSidebarPanel, feature_category: :navigation do
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:user) { project.first_owner }
+
+ let(:context) do
+ double("Stubbed context", current_user: user, container: project, project: project, current_ref: 'master').as_null_object # rubocop:disable RSpec/VerifiedDoubles
+ end
+
+ subject { described_class.new(context) }
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq(
+ {
+ title: project.name,
+ avatar: project.avatar_url,
+ id: project.id
+ })
+ end
+
+ describe '#renderable_menus' do
+ let(:category_menu) do
+ [
+ Sidebars::StaticMenu,
+ Sidebars::Projects::SuperSidebarMenus::PlanMenu,
+ Sidebars::Projects::Menus::RepositoryMenu,
+ Sidebars::Projects::Menus::CiCdMenu,
+ Sidebars::Projects::Menus::SecurityComplianceMenu,
+ Sidebars::Projects::SuperSidebarMenus::OperationsMenu,
+ Sidebars::Projects::Menus::MonitorMenu,
+ Sidebars::Projects::Menus::AnalyticsMenu,
+ Sidebars::UncategorizedMenu,
+ Sidebars::Projects::Menus::SettingsMenu
+ ]
+ end
+
+ it "is exposed as a renderable menu" do
+ expect(subject.instance_variable_get(:@menus).map(&:class)).to eq(category_menu)
+ end
+ end
+end
diff --git a/spec/lib/sidebars/static_menu_spec.rb b/spec/lib/sidebars/static_menu_spec.rb
new file mode 100644
index 00000000000..086eb332a15
--- /dev/null
+++ b/spec/lib/sidebars/static_menu_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::StaticMenu, feature_category: :navigation do
+ let(:context) { {} }
+
+ subject { described_class.new(context) }
+
+ describe '#serialize_for_super_sidebar' do
+ it 'returns flat list of all menu items' do
+ subject.add_item(Sidebars::MenuItem.new(title: 'Is active', link: 'foo2', active_routes: { controller: 'fooc' }))
+ subject.add_item(Sidebars::MenuItem.new(title: 'Not active', link: 'foo3', active_routes: { controller: 'barc' }))
+ subject.add_item(Sidebars::NilMenuItem.new(item_id: 'nil_item'))
+
+ allow(context).to receive(:route_is_active).and_return(->(x) { x[:controller] == 'fooc' })
+
+ expect(subject.serialize_for_super_sidebar).to eq(
+ [
+ {
+ title: "Is active",
+ icon: nil,
+ link: "foo2",
+ is_active: true,
+ pill_count: nil
+ },
+ {
+ title: "Not active",
+ icon: nil,
+ link: "foo3",
+ is_active: false,
+ pill_count: nil
+ }
+ ]
+ )
+ end
+ end
+end
diff --git a/spec/lib/sidebars/uncategorized_menu_spec.rb b/spec/lib/sidebars/uncategorized_menu_spec.rb
new file mode 100644
index 00000000000..45e7c0c87e2
--- /dev/null
+++ b/spec/lib/sidebars/uncategorized_menu_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UncategorizedMenu, feature_category: :navigation do
+ subject { described_class.new({}) }
+
+ it 'has title and sprite_icon' do
+ expect(subject.title).to eq(_("Uncategorized"))
+ expect(subject.sprite_icon).to eq("question")
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb
new file mode 100644
index 00000000000..44492380f11
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/activity_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::ActivityMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Activity'),
+ active_route: 'users#activity' do
+ let(:link) { "/users/#{user.username}/activity" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb
new file mode 100644
index 00000000000..a5371c36fd3
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/contributed_projects_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::ContributedProjectsMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Contributed projects'),
+ active_route: 'users#contributed' do
+ let(:link) { "/users/#{user.username}/contributed" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb
new file mode 100644
index 00000000000..1b3efbf4ceb
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/followers_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::FollowersMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Followers'),
+ active_route: 'users#followers' do
+ let(:link) { "/users/#{user.username}/followers" }
+ end
+
+ it_behaves_like 'Followers/followees counts', :followers
+end
diff --git a/spec/lib/sidebars/user_profile/menus/following_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/following_menu_spec.rb
new file mode 100644
index 00000000000..167961f085e
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/following_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::FollowingMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Following'),
+ active_route: 'users#following' do
+ let(:link) { "/users/#{user.username}/following" }
+ end
+
+ it_behaves_like 'Followers/followees counts', :followees
+end
diff --git a/spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb
new file mode 100644
index 00000000000..6c48ad2e8d0
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/groups_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::GroupsMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Groups'),
+ active_route: 'users#groups' do
+ let(:link) { "/users/#{user.username}/groups" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb
new file mode 100644
index 00000000000..e34f59cddf0
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/overview_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::OverviewMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Overview'),
+ active_route: 'users#show' do
+ let(:link) { "/#{user.username}" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb
new file mode 100644
index 00000000000..ba2c3d11b88
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/personal_projects_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::PersonalProjectsMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Personal projects'),
+ active_route: 'users#projects' do
+ let(:link) { "/users/#{user.username}/projects" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb
new file mode 100644
index 00000000000..2760d172a51
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/snippets_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::SnippetsMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Snippets'),
+ active_route: 'users#snippets' do
+ let(:link) { "/users/#{user.username}/snippets" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb b/spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb
new file mode 100644
index 00000000000..e205d1f5492
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/menus/starred_projects_menu_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Menus::StarredProjectsMenu, feature_category: :navigation do
+ it_behaves_like 'User profile menu',
+ title: s_('UserProfile|Starred projects'),
+ active_route: 'users#starred' do
+ let(:link) { "/users/#{user.username}/starred" }
+ end
+end
diff --git a/spec/lib/sidebars/user_profile/panel_spec.rb b/spec/lib/sidebars/user_profile/panel_spec.rb
new file mode 100644
index 00000000000..af261dce3f3
--- /dev/null
+++ b/spec/lib/sidebars/user_profile/panel_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserProfile::Panel, feature_category: :navigation do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ it 'implements #aria_label' do
+ expect(subject.aria_label).to eq(s_('UserProfile|User profile navigation'))
+ end
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq({
+ title: user.name,
+ avatar: user.avatar_url,
+ avatar_shape: 'circle'
+ })
+ end
+end
diff --git a/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb
new file mode 100644
index 00000000000..fa33e7bedfb
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/access_tokens_menu_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::AccessTokensMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/personal_access_tokens',
+ title: _('Access Tokens'),
+ icon: 'token',
+ active_routes: { controller: :personal_access_tokens }
+
+ describe '#render?' do
+ subject { described_class.new(context) }
+
+ let_it_be(:user) { build(:user) }
+
+ context 'when personal access tokens are disabled' do
+ before do
+ allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: true)
+ end
+
+ context 'when user is logged in' do
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+
+ context 'when personal access tokens are enabled' do
+ before do
+ allow(::Gitlab::CurrentSettings).to receive_messages(personal_access_tokens_disabled?: false)
+ end
+
+ context 'when user is logged in' do
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ it 'renders' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/user_settings/menus/account_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/account_menu_spec.rb
new file mode 100644
index 00000000000..d5810d9c5ae
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/account_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::AccountMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/account',
+ title: _('Account'),
+ icon: 'account',
+ active_routes: { controller: [:accounts, :two_factor_auths] }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb
new file mode 100644
index 00000000000..be5f826ee58
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::ActiveSessionsMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/active_sessions',
+ title: _('Active Sessions'),
+ icon: 'monitor-lines',
+ active_routes: { controller: :active_sessions }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb
new file mode 100644
index 00000000000..eeda4fb844c
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/applications_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::ApplicationsMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/applications',
+ title: _('Applications'),
+ icon: 'applications',
+ active_routes: { controller: 'oauth/applications' }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb
new file mode 100644
index 00000000000..33be5050c37
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/authentication_log_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::AuthenticationLogMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/audit_log',
+ title: _('Authentication Log'),
+ icon: 'log',
+ active_routes: { path: 'profiles#audit_log' }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb
new file mode 100644
index 00000000000..2a0587e2504
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/chat_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::ChatMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/chat',
+ title: _('Chat'),
+ icon: 'comment',
+ active_routes: { controller: :chat_names }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb
new file mode 100644
index 00000000000..2f16c68e601
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/emails_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::EmailsMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/emails',
+ title: _('Emails'),
+ icon: 'mail',
+ active_routes: { controller: :emails }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb
new file mode 100644
index 00000000000..1f4340ad29c
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/gpg_keys_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::GpgKeysMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/gpg_keys',
+ title: _('GPG Keys'),
+ icon: 'key',
+ active_routes: { controller: :gpg_keys }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb
new file mode 100644
index 00000000000..282324056d4
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/notifications_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::NotificationsMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/notifications',
+ title: _('Notifications'),
+ icon: 'notifications',
+ active_routes: { controller: :notifications }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/password_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/password_menu_spec.rb
new file mode 100644
index 00000000000..168019fea5d
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/password_menu_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::PasswordMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/password',
+ title: _('Password'),
+ icon: 'lock',
+ active_routes: { controller: :passwords }
+
+ describe '#render?' do
+ subject { described_class.new(context) }
+
+ let_it_be(:user) { build(:user) }
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ context 'when password authentication is enabled' do
+ before do
+ allow(user).to receive(:allow_password_authentication?).and_return(true)
+ end
+
+ it 'renders' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when password authentication is disabled' do
+ before do
+ allow(user).to receive(:allow_password_authentication?).and_return(false)
+ end
+
+ it 'renders' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb
new file mode 100644
index 00000000000..83a67a40081
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/preferences_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::PreferencesMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/preferences',
+ title: _('Preferences'),
+ icon: 'preferences',
+ active_routes: { controller: :preferences }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb
new file mode 100644
index 00000000000..8410ba7cfcd
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::ProfileMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile',
+ title: _('Profile'),
+ icon: 'profile',
+ active_routes: { path: 'profiles#show' }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb
new file mode 100644
index 00000000000..ea1a2a3539f
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/saved_replies_menu_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::SavedRepliesMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/saved_replies',
+ title: _('Saved Replies'),
+ icon: 'symlink',
+ active_routes: { controller: :saved_replies }
+
+ describe '#render?' do
+ subject { described_class.new(context) }
+
+ let_it_be(:user) { build(:user) }
+
+ context 'when saved replies are enabled' do
+ before do
+ allow(subject).to receive(:saved_replies_enabled?).and_return(true)
+ end
+
+ context 'when user is logged in' do
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ it 'does not render' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+
+ context 'when saved replies are disabled' do
+ before do
+ allow(subject).to receive(:saved_replies_enabled?).and_return(false)
+ end
+
+ context 'when user is logged in' do
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ it 'renders' do
+ expect(subject.render?).to be false
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb
new file mode 100644
index 00000000000..8c781cc743b
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/menus/ssh_keys_menu_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Menus::SshKeysMenu, feature_category: :navigation do
+ it_behaves_like 'User settings menu',
+ link: '/-/profile/keys',
+ title: _('SSH Keys'),
+ icon: 'key',
+ active_routes: { controller: :keys }
+
+ it_behaves_like 'User settings menu #render? method'
+end
diff --git a/spec/lib/sidebars/user_settings/panel_spec.rb b/spec/lib/sidebars/user_settings/panel_spec.rb
new file mode 100644
index 00000000000..aa05d99912a
--- /dev/null
+++ b/spec/lib/sidebars/user_settings/panel_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::UserSettings::Panel, feature_category: :navigation do
+ let_it_be(:user) { create(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq({ title: _('User settings'), avatar: user.avatar_url })
+ end
+end
diff --git a/spec/lib/sidebars/your_work/panel_spec.rb b/spec/lib/sidebars/your_work/panel_spec.rb
new file mode 100644
index 00000000000..97da94e4114
--- /dev/null
+++ b/spec/lib/sidebars/your_work/panel_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::YourWork::Panel, feature_category: :navigation do
+ let_it_be(:user) { create(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'implements #super_sidebar_context_header' do
+ expect(subject.super_sidebar_context_header).to eq({ title: 'Your work', icon: 'work' })
+ end
+end
diff --git a/spec/lib/unnested_in_filters/rewriter_spec.rb b/spec/lib/unnested_in_filters/rewriter_spec.rb
index bba27276037..fe34fba579b 100644
--- a/spec/lib/unnested_in_filters/rewriter_spec.rb
+++ b/spec/lib/unnested_in_filters/rewriter_spec.rb
@@ -69,21 +69,15 @@ RSpec.describe UnnestedInFilters::Rewriter do
let(:recorded_queries) { ActiveRecord::QueryRecorder.new { rewriter.rewrite.load } }
let(:relation) { User.where(state: :active, user_type: %i(support_bot alert_bot)).limit(2) }
- let(:users_default_select_fields) do
- User.default_select_columns
- .map { |field| "\"users\".\"#{field.name}\"" }
- .join(',')
- end
-
let(:expected_query) do
<<~SQL
SELECT
- #{users_default_select_fields}
+ "users".*
FROM
unnest('{1,2}'::smallint[]) AS "user_types"("user_type"),
LATERAL (
SELECT
- #{users_default_select_fields}
+ "users".*
FROM
"users"
WHERE
@@ -107,13 +101,13 @@ RSpec.describe UnnestedInFilters::Rewriter do
let(:expected_query) do
<<~SQL
SELECT
- #{users_default_select_fields}
+ "users".*
FROM
unnest(ARRAY(SELECT "users"."state" FROM "users")::character varying[]) AS "states"("state"),
unnest('{1,2}'::smallint[]) AS "user_types"("user_type"),
LATERAL (
SELECT
- #{users_default_select_fields}
+ "users".*
FROM
"users"
WHERE
@@ -135,12 +129,12 @@ RSpec.describe UnnestedInFilters::Rewriter do
let(:expected_query) do
<<~SQL
SELECT
- #{users_default_select_fields}
+ "users".*
FROM
unnest('{active,blocked,banned}'::charactervarying[]) AS "states"("state"),
LATERAL (
SELECT
- #{users_default_select_fields}
+ "users".*
FROM
"users"
WHERE
@@ -187,6 +181,8 @@ RSpec.describe UnnestedInFilters::Rewriter do
let(:expected_query) do
<<~SQL
+ SELECT
+ "users".*
FROM
"users"
WHERE
@@ -221,7 +217,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
it 'changes the query' do
- expect(issued_query.gsub(/\s/, '')).to include(expected_query.gsub(/\s/, ''))
+ expect(issued_query.gsub(/\s/, '')).to start_with(expected_query.gsub(/\s/, ''))
end
end
@@ -230,6 +226,8 @@ RSpec.describe UnnestedInFilters::Rewriter do
let(:expected_query) do
<<~SQL
+ SELECT
+ "users".*
FROM
"users"
WHERE
@@ -259,7 +257,7 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
it 'does not rewrite the in statement for the joined table' do
- expect(issued_query.gsub(/\s/, '')).to include(expected_query.gsub(/\s/, ''))
+ expect(issued_query.gsub(/\s/, '')).to start_with(expected_query.gsub(/\s/, ''))
end
end
diff --git a/spec/mailers/emails/in_product_marketing_spec.rb b/spec/mailers/emails/in_product_marketing_spec.rb
index 7c21e161ffe..5419c9e6798 100644
--- a/spec/mailers/emails/in_product_marketing_spec.rb
+++ b/spec/mailers/emails/in_product_marketing_spec.rb
@@ -10,11 +10,7 @@ RSpec.describe Emails::InProductMarketing do
let_it_be(:user) { create(:user) }
shared_examples 'has custom headers when on gitlab.com' do
- context 'when on gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'when on gitlab.com', :saas do
it 'has custom headers' do
aggregate_failures do
expect(subject).to deliver_from(described_class::FROM_ADDRESS)
diff --git a/spec/mailers/emails/issues_spec.rb b/spec/mailers/emails/issues_spec.rb
index 21e07c0252d..b5f3972f38e 100644
--- a/spec/mailers/emails/issues_spec.rb
+++ b/spec/mailers/emails/issues_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'email_spec'
-RSpec.describe Emails::Issues do
+RSpec.describe Emails::Issues, feature_category: :team_planning do
include EmailSpec::Matchers
it 'adds email methods to Notify' do
@@ -54,38 +54,6 @@ RSpec.describe Emails::Issues do
subject { Notify.issues_csv_email(user, empty_project, "dummy content", export_status) }
- include_context 'gitlab email notification'
-
- it 'attachment has csv mime type' do
- expect(attachment.mime_type).to eq 'text/csv'
- end
-
- it 'generates a useful filename' do
- expect(attachment.filename).to include(Date.today.year.to_s)
- expect(attachment.filename).to include('issues')
- expect(attachment.filename).to include('myproject')
- expect(attachment.filename).to end_with('.csv')
- end
-
- it 'mentions number of issues and project name' do
- expect(subject).to have_content '3'
- expect(subject).to have_content empty_project.name
- end
-
- it "doesn't need to mention truncation by default" do
- expect(subject).not_to have_content 'truncated'
- end
-
- context 'when truncated' do
- let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
-
- it 'mentions that the csv has been truncated' do
- expect(subject).to have_content 'truncated'
- end
-
- it 'mentions the number of issues written and expected' do
- expect(subject).to have_content '10 of 12 issues'
- end
- end
+ it_behaves_like 'export csv email', 'issues'
end
end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index f5fce559778..c796801fdf9 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -429,11 +429,16 @@ RSpec.describe Emails::Profile do
is_expected.to have_subject "#{Gitlab.config.gitlab.host} sign-in from new location"
end
+ it 'mentions the username' do
+ is_expected.to have_body_text user.name
+ is_expected.to have_body_text user.username
+ end
+
it 'mentions the new sign-in IP' do
is_expected.to have_body_text ip
end
- it 'mentioned the time' do
+ it 'mentions the time' do
is_expected.to have_body_text current_time.strftime('%Y-%m-%d %H:%M:%S %Z')
end
@@ -476,7 +481,7 @@ RSpec.describe Emails::Profile do
end
it 'has the correct subject' do
- is_expected.to have_subject "Attempted sign in to #{Gitlab.config.gitlab.host} using a wrong two-factor authentication code"
+ is_expected.to have_subject "Attempted sign in to #{Gitlab.config.gitlab.host} using an incorrect verification code"
end
it 'mentions the IP address' do
diff --git a/spec/mailers/emails/work_items_spec.rb b/spec/mailers/emails/work_items_spec.rb
new file mode 100644
index 00000000000..eb2c751388d
--- /dev/null
+++ b/spec/mailers/emails/work_items_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+RSpec.describe Emails::WorkItems, feature_category: :team_planning do
+ describe '#export_work_items_csv_email' do
+ let(:user) { build_stubbed(:user) }
+ let(:empty_project) { build_stubbed(:project, path: 'myproject') }
+ let(:export_status) { { truncated: false, rows_expected: 3, rows_written: 3 } }
+ let(:attachment) { subject.attachments.first }
+
+ subject { Notify.export_work_items_csv_email(user, empty_project, "dummy content", export_status) }
+
+ it_behaves_like 'export csv email', 'work_items'
+ end
+end
diff --git a/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb b/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
index 0d89851cac1..56482e8bd25 100644
--- a/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
+++ b/spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildNeeds, feature_category: :pipeline_authoring do
+RSpec.describe DropTemporaryColumnsAndTriggersForCiBuildNeeds, feature_category: :pipeline_composition do
let(:ci_build_needs_table) { table(:ci_build_needs) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb b/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
index 6b0c3a6db9a..5a3ba16fcc0 100644
--- a/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
+++ b/spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe DropInt4ColumnForCiSourcesPipelines, feature_category: :pipeline_authoring do
+RSpec.describe DropInt4ColumnForCiSourcesPipelines, feature_category: :pipeline_composition do
let(:ci_sources_pipelines) { table(:ci_sources_pipelines) }
it 'correctly migrates up and down' do
diff --git a/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb b/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
index bfe2b661a31..ede9c5ea7e8 100644
--- a/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
+++ b/spec/migrations/20211117084814_migrate_remaining_u2f_registrations_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe MigrateRemainingU2fRegistrations, :migration, feature_category: :authentication_and_authorization do
+RSpec.describe MigrateRemainingU2fRegistrations, :migration, feature_category: :system_access do
let(:u2f_registrations) { table(:u2f_registrations) }
let(:webauthn_registrations) { table(:webauthn_registrations) }
let(:users) { table(:users) }
diff --git a/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb b/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb
index 735232dfac7..b03849b61a2 100644
--- a/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb
+++ b/spec/migrations/20220513043344_reschedule_expire_o_auth_tokens_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe RescheduleExpireOAuthTokens, feature_category: :authentication_and_authorization do
+RSpec.describe RescheduleExpireOAuthTokens, feature_category: :system_access do
let!(:migration) { described_class::MIGRATION }
describe '#up' do
diff --git a/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb b/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb
index 1b8ec47f61b..2eff65d5873 100644
--- a/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb
+++ b/spec/migrations/20220601152916_add_user_id_and_ip_address_success_index_to_authentication_events_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
RSpec.describe AddUserIdAndIpAddressSuccessIndexToAuthenticationEvents,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
let(:db) { described_class.new }
let(:old_index) { described_class::OLD_INDEX_NAME }
let(:new_index) { described_class::NEW_INDEX_NAME }
diff --git a/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb b/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
index 19cf3b2fb69..7b0df403e30 100644
--- a/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
+++ b/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
require_migration!
RSpec.describe RemoveOrphanGroupTokenUsers, :migration, :sidekiq_inline,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
subject(:migration) { described_class.new }
let(:users) { table(:users) }
diff --git a/spec/migrations/20221209235940_cleanup_o_auth_access_tokens_with_null_expires_in_spec.rb b/spec/migrations/20221209235940_cleanup_o_auth_access_tokens_with_null_expires_in_spec.rb
index da6532a822a..e5890ffce17 100644
--- a/spec/migrations/20221209235940_cleanup_o_auth_access_tokens_with_null_expires_in_spec.rb
+++ b/spec/migrations/20221209235940_cleanup_o_auth_access_tokens_with_null_expires_in_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe CleanupOAuthAccessTokensWithNullExpiresIn, feature_category: :authentication_and_authorization do
+RSpec.describe CleanupOAuthAccessTokensWithNullExpiresIn, feature_category: :system_access do
let(:batched_migration) { described_class::MIGRATION }
it 'schedules background jobs for each batch of oauth_access_tokens' do
diff --git a/spec/migrations/20230118144623_schedule_migration_for_remediation_spec.rb b/spec/migrations/20230118144623_schedule_migration_for_remediation_spec.rb
new file mode 100644
index 00000000000..f6d0f32b87c
--- /dev/null
+++ b/spec/migrations/20230118144623_schedule_migration_for_remediation_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleMigrationForRemediation, :migration, feature_category: :vulnerability_management do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules a batched background migration' do
+ migrate!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ 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/20230125195503_queue_backfill_compliance_violations_spec.rb b/spec/migrations/20230125195503_queue_backfill_compliance_violations_spec.rb
new file mode 100644
index 00000000000..a70f9820855
--- /dev/null
+++ b/spec/migrations/20230125195503_queue_backfill_compliance_violations_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillComplianceViolations, feature_category: :compliance_management do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of merge_request_compliance_violations' do
+ migrate!
+
+ expect(migration).to(
+ have_scheduled_batched_migration(
+ table_name: :merge_requests_compliance_violations,
+ column_name: :id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::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/20230130182412_schedule_create_vulnerability_links_migration_spec.rb b/spec/migrations/20230130182412_schedule_create_vulnerability_links_migration_spec.rb
new file mode 100644
index 00000000000..58e27379ef7
--- /dev/null
+++ b/spec/migrations/20230130182412_schedule_create_vulnerability_links_migration_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleCreateVulnerabilityLinksMigration, feature_category: :vulnerability_management do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of Vulnerabilities::Feedback' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :vulnerability_feedback,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::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/20230202211434_migrate_redis_slot_keys_spec.rb b/spec/migrations/20230202211434_migrate_redis_slot_keys_spec.rb
new file mode 100644
index 00000000000..ca2c50241bf
--- /dev/null
+++ b/spec/migrations/20230202211434_migrate_redis_slot_keys_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MigrateRedisSlotKeys, :migration, feature_category: :service_ping do
+ let(:date) { Date.yesterday.strftime('%G-%j') }
+ let(:week) { Date.yesterday.strftime('%G-%V') }
+
+ before do
+ allow(described_class::BackupHLLRedisCounter).to receive(:known_events).and_return([{
+ redis_slot: 'analytics',
+ aggregation: 'daily',
+ name: 'users_viewing_analytics_group_devops_adoption'
+ }, {
+ aggregation: 'weekly',
+ name: 'wiki_action'
+ }])
+ end
+
+ describe "#up" do
+ it 'rename keys', :aggregate_failures do
+ expiry_daily = described_class::BackupHLLRedisCounter::DEFAULT_DAILY_KEY_EXPIRY_LENGTH
+ expiry_weekly = described_class::BackupHLLRedisCounter::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH
+
+ default_slot = described_class::BackupHLLRedisCounter::REDIS_SLOT
+
+ old_slot_a = "#{date}-users_viewing_{analytics}_group_devops_adoption"
+ old_slot_b = "{wiki_action}-#{week}"
+
+ new_slot_a = "#{date}-{#{default_slot}}_users_viewing_analytics_group_devops_adoption"
+ new_slot_b = "{#{default_slot}}_wiki_action-#{week}"
+
+ Gitlab::Redis::HLL.add(key: old_slot_a, value: 1, expiry: expiry_daily)
+ Gitlab::Redis::HLL.add(key: old_slot_b, value: 1, expiry: expiry_weekly)
+
+ # check that we merge values during migration
+ # i.e. we dont drop keys created after code deploy but before the migration
+ Gitlab::Redis::HLL.add(key: new_slot_a, value: 2, expiry: expiry_daily)
+ Gitlab::Redis::HLL.add(key: new_slot_b, value: 2, expiry: expiry_weekly)
+
+ migrate!
+
+ expect(Gitlab::Redis::HLL.count(keys: new_slot_a)).to eq(2)
+ expect(Gitlab::Redis::HLL.count(keys: new_slot_b)).to eq(2)
+ expect(with_redis { |r| r.ttl(new_slot_a) }).to be_within(600).of(expiry_daily)
+ expect(with_redis { |r| r.ttl(new_slot_b) }).to be_within(600).of(expiry_weekly)
+ end
+ end
+
+ def with_redis(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+end
diff --git a/spec/migrations/20230208125736_schedule_migration_for_links_spec.rb b/spec/migrations/20230208125736_schedule_migration_for_links_spec.rb
new file mode 100644
index 00000000000..dd1c30415a4
--- /dev/null
+++ b/spec/migrations/20230208125736_schedule_migration_for_links_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleMigrationForLinks, :migration, feature_category: :vulnerability_management 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: :vulnerability_occurrences,
+ column_name: :id,
+ interval: described_class::DELAY_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
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/20230214181633_finalize_ci_build_needs_big_int_conversion_spec.rb b/spec/migrations/20230214181633_finalize_ci_build_needs_big_int_conversion_spec.rb
new file mode 100644
index 00000000000..638fe2a12d5
--- /dev/null
+++ b/spec/migrations/20230214181633_finalize_ci_build_needs_big_int_conversion_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinalizeCiBuildNeedsBigIntConversion, migration: :gitlab_ci, feature_category: :continuous_integration do
+ describe '#up' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:dot_com, :dev_or_test, :jh, :expectation) do
+ true | true | true | :not_to
+ true | false | true | :not_to
+ false | true | true | :not_to
+ false | false | true | :not_to
+ true | true | false | :to
+ true | false | false | :to
+ false | true | false | :to
+ false | false | false | :not_to
+ end
+
+ with_them do
+ it 'ensures the migration is completed for GitLab.com, dev, or test' do
+ allow(Gitlab).to receive(:com?).and_return(dot_com)
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test)
+ allow(Gitlab).to receive(:jh?).and_return(jh)
+
+ migration_arguments = {
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: 'ci_build_needs',
+ column_name: 'id',
+ job_arguments: [['id'], ['id_convert_to_bigint']]
+ }
+
+ expect(described_class).send(
+ expectation,
+ ensure_batched_background_migration_is_finished_for(migration_arguments)
+ )
+
+ migrate!
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230220102212_swap_columns_ci_build_needs_big_int_conversion_spec.rb b/spec/migrations/20230220102212_swap_columns_ci_build_needs_big_int_conversion_spec.rb
new file mode 100644
index 00000000000..1c21047c0c3
--- /dev/null
+++ b/spec/migrations/20230220102212_swap_columns_ci_build_needs_big_int_conversion_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapColumnsCiBuildNeedsBigIntConversion, feature_category: :continuous_integration do
+ describe '#up' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:dot_com, :dev_or_test, :jh, :swap) do
+ true | true | true | false
+ true | false | true | false
+ false | true | true | false
+ false | false | true | false
+ true | true | false | true
+ true | false | false | true
+ false | true | false | true
+ false | false | false | false
+ end
+
+ with_them do
+ before do
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id TYPE integer')
+ connection.execute('ALTER TABLE ci_build_needs ALTER COLUMN id_convert_to_bigint TYPE bigint')
+ end
+
+ it 'swaps the integer and bigint columns for GitLab.com, dev, or test' do
+ allow(Gitlab).to receive(:com?).and_return(dot_com)
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test)
+ allow(Gitlab).to receive(:jh?).and_return(jh)
+
+ ci_build_needs = table(:ci_build_needs)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ ci_build_needs.reset_column_information
+
+ expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('integer')
+ expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ ci_build_needs.reset_column_information
+
+ if swap
+ expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }.sql_type).to eq('integer')
+ else
+ expect(ci_build_needs.columns.find { |c| c.name == 'id' }.sql_type).to eq('integer')
+ expect(ci_build_needs.columns.find { |c| c.name == 'id_convert_to_bigint' }.sql_type).to eq('bigint')
+ end
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb b/spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb
new file mode 100644
index 00000000000..cbf6b2882c4
--- /dev/null
+++ b/spec/migrations/20230221093533_add_tmp_partial_index_on_vulnerability_report_types_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+require_migration!
+
+RSpec.describe AddTmpPartialIndexOnVulnerabilityReportTypes, feature_category: :vulnerability_management do
+ let(:async_index) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex }
+ let(:index_name) { described_class::INDEX_NAME }
+
+ it "schedules the index" do
+ reversible_migration do |migration|
+ migration.before -> do
+ expect(async_index.where(name: index_name).count).to be(0)
+ end
+
+ migration.after -> do
+ expect(async_index.where(name: index_name).count).to be(1)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230221214519_remove_incorrectly_onboarded_namespaces_from_onboarding_progress_spec.rb b/spec/migrations/20230221214519_remove_incorrectly_onboarded_namespaces_from_onboarding_progress_spec.rb
new file mode 100644
index 00000000000..be49a3e919d
--- /dev/null
+++ b/spec/migrations/20230221214519_remove_incorrectly_onboarded_namespaces_from_onboarding_progress_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RemoveIncorrectlyOnboardedNamespacesFromOnboardingProgress, feature_category: :onboarding do
+ let(:onboarding_progresses) { table(:onboarding_progresses) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ # namespace to keep with name Learn Gitlab
+ let(:namespace1) { namespaces.create!(name: 'namespace1', type: 'Group', path: 'namespace1') }
+ let!(:onboard_keep_1) { onboarding_progresses.create!(namespace_id: namespace1.id) }
+ let!(:proj1) do
+ proj_namespace = namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id)
+ projects.create!(name: 'project', namespace_id: namespace1.id, project_namespace_id: proj_namespace.id)
+ end
+
+ let!(:learn_gitlab) do
+ proj_namespace = namespaces.create!(name: 'projlg1', path: 'projlg1', type: 'Project', parent_id: namespace1.id)
+ projects.create!(name: 'Learn GitLab', namespace_id: namespace1.id, project_namespace_id: proj_namespace.id)
+ end
+
+ # namespace to keep with name Learn GitLab - Ultimate trial
+ let(:namespace2) { namespaces.create!(name: 'namespace2', type: 'Group', path: 'namespace2') }
+ let!(:onboard_keep_2) { onboarding_progresses.create!(namespace_id: namespace2.id) }
+ let!(:proj2) do
+ proj_namespace = namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace2.id)
+ projects.create!(name: 'project', namespace_id: namespace2.id, project_namespace_id: proj_namespace.id)
+ end
+
+ let!(:learn_gitlab2) do
+ proj_namespace = namespaces.create!(name: 'projlg2', path: 'projlg2', type: 'Project', parent_id: namespace2.id)
+ projects.create!(
+ name: 'Learn GitLab - Ultimate trial',
+ namespace_id: namespace2.id,
+ project_namespace_id: proj_namespace.id
+ )
+ end
+
+ # namespace to remove without learn gitlab project
+ let(:namespace3) { namespaces.create!(name: 'namespace3', type: 'Group', path: 'namespace3') }
+ let!(:onboarding_to_delete) { onboarding_progresses.create!(namespace_id: namespace3.id) }
+ let!(:proj3) do
+ proj_namespace = namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: namespace3.id)
+ projects.create!(name: 'project', namespace_id: namespace3.id, project_namespace_id: proj_namespace.id)
+ end
+
+ # namespace to remove without any projects
+ let(:namespace4) { namespaces.create!(name: 'namespace4', type: 'Group', path: 'namespace4') }
+ let!(:onboarding_to_delete_without_project) { onboarding_progresses.create!(namespace_id: namespace4.id) }
+
+ describe '#up' do
+ it 'deletes the onboarding for namespaces without learn gitlab' do
+ expect { migrate! }.to change { onboarding_progresses.count }.by(-2)
+ expect(onboarding_progresses.all).to contain_exactly(onboard_keep_1, onboard_keep_2)
+ end
+ end
+end
diff --git a/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb b/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb
new file mode 100644
index 00000000000..9163c30fe30
--- /dev/null
+++ b/spec/migrations/20230223065753_finalize_nullify_creator_id_of_orphaned_projects_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe FinalizeNullifyCreatorIdOfOrphanedProjects, :migration, feature_category: :projects do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+ let(:batch_failed_status) { 2 }
+ let(:batch_finalized_status) { 3 }
+
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ shared_examples 'finalizes the migration' do
+ it 'finalizes the migration' do
+ expect do
+ migrate!
+
+ migration_record.reload
+ failed_job.reload
+ end.to change { migration_record.status }.from(migration_record.status).to(3).and(
+ change { failed_job.status }.from(batch_failed_status).to(batch_finalized_status)
+ )
+ end
+ end
+
+ context 'when migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:migration_record) do
+ batched_migrations.create!(
+ job_class_name: 'NullifyCreatorIdColumnOfOrphanedProjects',
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 500,
+ max_batch_size: 5000,
+ gitlab_schema: :gitlab_main,
+ status: 3 # finished
+ )
+ end
+
+ context 'when migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses', :redis do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ let!(:failed_job) do
+ table(:batched_background_migration_jobs).create!(
+ batched_background_migration_id: migration_record.id,
+ status: batch_failed_status,
+ min_value: 1,
+ max_value: 10,
+ attempts: 2,
+ batch_size: 100,
+ sub_batch_size: 10
+ )
+ end
+
+ before do
+ migration_record.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230224085743_update_issues_internal_id_scope_spec.rb b/spec/migrations/20230224085743_update_issues_internal_id_scope_spec.rb
new file mode 100644
index 00000000000..7c7b58c7f0e
--- /dev/null
+++ b/spec/migrations/20230224085743_update_issues_internal_id_scope_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateIssuesInternalIdScope, feature_category: :team_planning do
+ describe '#up' do
+ it 'schedules background migration' do
+ migrate!
+
+ expect(described_class::MIGRATION).to have_scheduled_batched_migration(
+ table_name: :internal_ids,
+ column_name: :id,
+ interval: described_class::INTERVAL)
+ end
+ end
+
+ describe '#down' do
+ it 'does not schedule background migration' do
+ schema_migrate_down!
+
+ expect(described_class::MIGRATION).not_to have_scheduled_batched_migration(
+ table_name: :internal_ids,
+ column_name: :id,
+ interval: described_class::INTERVAL)
+ end
+ end
+end
diff --git a/spec/migrations/20230224144233_migrate_evidences_from_raw_metadata_spec.rb b/spec/migrations/20230224144233_migrate_evidences_from_raw_metadata_spec.rb
new file mode 100644
index 00000000000..9b38557c8c3
--- /dev/null
+++ b/spec/migrations/20230224144233_migrate_evidences_from_raw_metadata_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe MigrateEvidencesFromRawMetadata, :migration, feature_category: :vulnerability_management 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: :vulnerability_occurrences,
+ column_name: :id,
+ interval: described_class::DELAY_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
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb b/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb
new file mode 100644
index 00000000000..065b6d00ddb
--- /dev/null
+++ b/spec/migrations/20230228142350_add_notifications_work_item_widget_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddNotificationsWorkItemWidget, :migration, feature_category: :team_planning do
+ let(:migration) { described_class.new }
+ let(:work_item_definitions) { table(:work_item_widget_definitions) }
+
+ describe '#up' do
+ it 'creates notifications widget definition in all types' do
+ work_item_definitions.where(name: 'Notifications').delete_all
+
+ expect { migrate! }.to change { work_item_definitions.count }.by(7)
+ expect(work_item_definitions.all.pluck(:name)).to include('Notifications')
+ end
+ end
+
+ describe '#down' do
+ it 'removes definitions for notifications widget' do
+ migrate!
+
+ expect { migration.down }.to change { work_item_definitions.count }.by(-7)
+ expect(work_item_definitions.all.pluck(:name)).not_to include('Notifications')
+ end
+ end
+end
diff --git a/spec/migrations/20230302185739_queue_fix_vulnerability_reads_has_issues_spec.rb b/spec/migrations/20230302185739_queue_fix_vulnerability_reads_has_issues_spec.rb
new file mode 100644
index 00000000000..54b1e231db2
--- /dev/null
+++ b/spec/migrations/20230302185739_queue_fix_vulnerability_reads_has_issues_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueFixVulnerabilityReadsHasIssues, feature_category: :vulnerability_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a 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: :vulnerability_issue_links,
+ column_name: :vulnerability_id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20230303105806_queue_delete_orphaned_packages_dependencies_spec.rb b/spec/migrations/20230303105806_queue_delete_orphaned_packages_dependencies_spec.rb
new file mode 100644
index 00000000000..7fe90a08763
--- /dev/null
+++ b/spec/migrations/20230303105806_queue_delete_orphaned_packages_dependencies_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueDeleteOrphanedPackagesDependencies, feature_category: :package_registry do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a 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: :packages_dependencies,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20230306195007_queue_backfill_project_wiki_repositories_spec.rb b/spec/migrations/20230306195007_queue_backfill_project_wiki_repositories_spec.rb
new file mode 100644
index 00000000000..07f501a3f98
--- /dev/null
+++ b/spec/migrations/20230306195007_queue_backfill_project_wiki_repositories_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillProjectWikiRepositories, feature_category: :geo_replication do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a 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: :projects,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20230309071242_delete_security_policy_bot_users_spec.rb b/spec/migrations/20230309071242_delete_security_policy_bot_users_spec.rb
new file mode 100644
index 00000000000..4dd44cad158
--- /dev/null
+++ b/spec/migrations/20230309071242_delete_security_policy_bot_users_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe DeleteSecurityPolicyBotUsers, feature_category: :security_policy_management do
+ let(:users) { table(:users) }
+
+ before do
+ users.create!(user_type: 10, projects_limit: 0, email: 'security_policy_bot@example.com')
+ users.create!(user_type: 1, projects_limit: 0, email: 'support_bot@example.com')
+ users.create!(projects_limit: 0, email: 'human@example.com')
+ end
+
+ describe '#up' do
+ it 'deletes security_policy_bot users' do
+ expect { migrate! }.to change { users.count }.by(-1)
+
+ expect(users.where(user_type: 10).count).to eq(0)
+ expect(users.where(user_type: 1).count).to eq(1)
+ expect(users.where(user_type: nil).count).to eq(1)
+ end
+ end
+end
diff --git a/spec/migrations/20230313150531_reschedule_migration_for_remediation_spec.rb b/spec/migrations/20230313150531_reschedule_migration_for_remediation_spec.rb
new file mode 100644
index 00000000000..00f0836285b
--- /dev/null
+++ b/spec/migrations/20230313150531_reschedule_migration_for_remediation_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RescheduleMigrationForRemediation, :migration, feature_category: :vulnerability_management 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: :vulnerability_occurrences,
+ column_name: :id,
+ interval: described_class::DELAY_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
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb b/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb
index 5029a861d31..83b47da3065 100644
--- a/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb
+++ b/spec/migrations/backfill_integrations_enable_ssl_verification_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe BackfillIntegrationsEnableSslVerification, feature_category: :authentication_and_authorization do
+RSpec.describe BackfillIntegrationsEnableSslVerification, feature_category: :system_access do
let!(:migration) { described_class::MIGRATION }
let!(:integrations) { described_class::Integration }
diff --git a/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb b/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb
index 7aaa90ee985..5854dcd3cb0 100644
--- a/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb
+++ b/spec/migrations/cleanup_backfill_integrations_enable_ssl_verification_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
RSpec.describe CleanupBackfillIntegrationsEnableSslVerification, :migration,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
let(:job_class_name) { 'BackfillIntegrationsEnableSslVerification' }
before do
diff --git a/spec/migrations/ensure_merge_request_metrics_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb b/spec/migrations/ensure_merge_request_metrics_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..118e0058922
--- /dev/null
+++ b/spec/migrations/ensure_merge_request_metrics_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EnsureMergeRequestMetricsIdBigintBackfillIsFinishedForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:migration_arguments) do
+ {
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: 'merge_request_metrics',
+ column_name: 'id',
+ job_arguments: [['id'], ['id_convert_to_bigint']]
+ }
+ end
+
+ it 'ensures the migration is completed for GitLab.com, dev, or test' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
+ expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)
+ end
+
+ migrate!
+ end
+
+ it 'skips the check for other instances' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
+ expect(instance).not_to receive(:ensure_batched_background_migration_is_finished)
+ end
+
+ migrate!
+ end
+ end
+end
diff --git a/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb b/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..9066413ce68
--- /dev/null
+++ b/spec/migrations/ensure_timelogs_note_id_bigint_backfill_is_finished_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EnsureTimelogsNoteIdBigintBackfillIsFinishedForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:migration_arguments) do
+ {
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: 'timelogs',
+ column_name: 'id',
+ job_arguments: [['note_id'], ['note_id_convert_to_bigint']]
+ }
+ end
+
+ it 'ensures the migration is completed for GitLab.com, dev, or test' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
+ expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)
+ end
+
+ migrate!
+ end
+
+ it 'skips the check for other instances' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
+ expect(instance).not_to receive(:ensure_batched_background_migration_is_finished)
+ end
+
+ migrate!
+ end
+ end
+end
diff --git a/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb b/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb
index 8209f317550..068da23113d 100644
--- a/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb
+++ b/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillAdminModeScopeForPersonalAccessTokens,
- feature_category: :authentication_and_authorization do
+ feature_category: :system_access do
describe '#up' do
it 'schedules background migration' do
migrate!
diff --git a/spec/migrations/queue_backfill_prepared_at_data_spec.rb b/spec/migrations/queue_backfill_prepared_at_data_spec.rb
new file mode 100644
index 00000000000..ac3ea2f59c5
--- /dev/null
+++ b/spec/migrations/queue_backfill_prepared_at_data_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillPreparedAtData, feature_category: :code_review_workflow do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a 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: :merge_requests,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/swap_merge_request_metrics_id_to_bigint_for_gitlab_dot_com_spec.rb b/spec/migrations/swap_merge_request_metrics_id_to_bigint_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..b819495aa89
--- /dev/null
+++ b/spec/migrations/swap_merge_request_metrics_id_to_bigint_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapMergeRequestMetricsIdToBigintForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ before do
+ # As we call `schema_migrate_down!` before each example, and for this migration
+ # `#down` is same as `#up`, we need to ensure we start from the expected state.
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE merge_request_metrics ALTER COLUMN id TYPE integer')
+ connection.execute('ALTER TABLE merge_request_metrics ALTER COLUMN id_convert_to_bigint TYPE bigint')
+ end
+
+ it 'swaps the integer and bigint columns for GitLab.com, dev, or test' do
+ # rubocop: disable RSpec/AnyInstanceOf
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
+ # rubocop: enable RSpec/AnyInstanceOf
+
+ merge_request_metrics = table(:merge_request_metrics)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ merge_request_metrics.reset_column_information
+
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id' }.sql_type).to eq('integer')
+ expect(merge_request_metrics.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ merge_request_metrics.reset_column_information
+
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
+ expect(merge_request_metrics.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('integer')
+ }
+ end
+ end
+ end
+
+ it 'is a no-op for other instances' do
+ # rubocop: disable RSpec/AnyInstanceOf
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
+ # rubocop: enable RSpec/AnyInstanceOf
+
+ merge_request_metrics = table(:merge_request_metrics)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ merge_request_metrics.reset_column_information
+
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id' }.sql_type).to eq('integer')
+ expect(merge_request_metrics.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ merge_request_metrics.reset_column_information
+
+ expect(merge_request_metrics.columns.find { |c| c.name == 'id' }.sql_type).to eq('integer')
+ expect(merge_request_metrics.columns.find do |c|
+ c.name == 'id_convert_to_bigint'
+ end.sql_type).to eq('bigint')
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/swap_timelogs_note_id_to_bigint_for_gitlab_dot_com_spec.rb b/spec/migrations/swap_timelogs_note_id_to_bigint_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..708688ec446
--- /dev/null
+++ b/spec/migrations/swap_timelogs_note_id_to_bigint_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapTimelogsNoteIdToBigintForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ before do
+ # A we call `schema_migrate_down!` before each example, and for this migration
+ # `#down` is same as `#up`, we need to ensure we start from the expected state.
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE timelogs ALTER COLUMN note_id TYPE integer')
+ connection.execute('ALTER TABLE timelogs ALTER COLUMN note_id_convert_to_bigint TYPE bigint')
+ end
+
+ # rubocop: disable RSpec/AnyInstanceOf
+ it 'swaps the integer and bigint columns for GitLab.com, dev, or test' do
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
+
+ timelogs = table(:timelogs)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ timelogs.reset_column_information
+
+ expect(timelogs.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(timelogs.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ timelogs.reset_column_information
+
+ expect(timelogs.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('bigint')
+ expect(timelogs.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('integer')
+ }
+ end
+ end
+ end
+
+ it 'is a no-op for other instances' do
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
+
+ timelogs = table(:timelogs)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ timelogs.reset_column_information
+
+ expect(timelogs.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(timelogs.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ timelogs.reset_column_information
+
+ expect(timelogs.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(timelogs.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+ end
+ end
+ end
+ # rubocop: enable RSpec/AnyInstanceOf
+ end
+end
diff --git a/spec/migrations/update_application_settings_protected_paths_spec.rb b/spec/migrations/update_application_settings_protected_paths_spec.rb
index d61eadf9f9c..055955c56f1 100644
--- a/spec/migrations/update_application_settings_protected_paths_spec.rb
+++ b/spec/migrations/update_application_settings_protected_paths_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require_migration!
RSpec.describe UpdateApplicationSettingsProtectedPaths, :aggregate_failures,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
subject(:migration) { described_class.new }
let!(:application_settings) { table(:application_settings) }
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 7995cc36383..9026a870138 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -70,6 +70,42 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
}
end
+ describe 'scopes' do
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:report1) { create(:abuse_report, reporter: reporter) }
+ let_it_be(:report2) { create(:abuse_report, :closed, category: 'phishing') }
+
+ describe '.by_reporter_id' do
+ subject(:results) { described_class.by_reporter_id(reporter.id) }
+
+ it 'returns reports with reporter_id equal to the given user id' do
+ expect(subject).to match_array([report1])
+ end
+ end
+
+ describe '.open' do
+ subject(:results) { described_class.open }
+
+ it 'returns reports without resolved_at value' do
+ expect(subject).to match_array([report, report1])
+ end
+ end
+
+ describe '.closed' do
+ subject(:results) { described_class.closed }
+
+ it 'returns reports with resolved_at value' do
+ expect(subject).to match_array([report2])
+ end
+ end
+
+ describe '.by_category' do
+ it 'returns abuse reports with the specified category' do
+ expect(described_class.by_category('phishing')).to match_array([report2])
+ end
+ end
+ end
+
describe 'before_validation' do
context 'when links to spam contains empty strings' do
let(:report) { create(:abuse_report, links_to_spam: ['', 'https://gitlab.com']) }
diff --git a/spec/models/achievements/user_achievement_spec.rb b/spec/models/achievements/user_achievement_spec.rb
index 9d88bfdd477..41eaadafa67 100644
--- a/spec/models/achievements/user_achievement_spec.rb
+++ b/spec/models/achievements/user_achievement_spec.rb
@@ -7,7 +7,34 @@ RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :u
it { is_expected.to belong_to(:achievement).inverse_of(:user_achievements).required }
it { is_expected.to belong_to(:user).inverse_of(:user_achievements).required }
- it { is_expected.to belong_to(:awarded_by_user).class_name('User').inverse_of(:awarded_user_achievements).optional }
+ it { is_expected.to belong_to(:awarded_by_user).class_name('User').inverse_of(:awarded_user_achievements).required }
it { is_expected.to belong_to(:revoked_by_user).class_name('User').inverse_of(:revoked_user_achievements).optional }
+
+ describe '#revoked?' do
+ subject { achievement.revoked? }
+
+ context 'when revoked' do
+ let_it_be(:achievement) { create(:user_achievement, :revoked) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when not revoked' do
+ let_it_be(:achievement) { create(:user_achievement) }
+
+ it { is_expected.to be false }
+ end
+ end
+ end
+
+ describe 'scopes' do
+ let_it_be(:user_achievement) { create(:user_achievement) }
+ let_it_be(:revoked_user_achievement) { create(:user_achievement, :revoked) }
+
+ describe '.not_revoked' do
+ it 'only returns user achievements which have not been revoked' do
+ expect(described_class.not_revoked).to contain_exactly(user_achievement)
+ end
+ end
end
end
diff --git a/spec/models/airflow/dags_spec.rb b/spec/models/airflow/dags_spec.rb
deleted file mode 100644
index ff3c4522779..00000000000
--- a/spec/models/airflow/dags_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Airflow::Dags, feature_category: :dataops do
- describe 'associations' do
- it { is_expected.to belong_to(:project) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:project) }
- it { is_expected.to validate_presence_of(:dag_name) }
- it { is_expected.to validate_length_of(:dag_name).is_at_most(255) }
- it { is_expected.to validate_length_of(:schedule).is_at_most(255) }
- it { is_expected.to validate_length_of(:fileloc).is_at_most(255) }
- end
-end
diff --git a/spec/models/alert_management/alert_assignee_spec.rb b/spec/models/alert_management/alert_assignee_spec.rb
index c50a3ec0d01..647195380b3 100644
--- a/spec/models/alert_management/alert_assignee_spec.rb
+++ b/spec/models/alert_management/alert_assignee_spec.rb
@@ -5,7 +5,11 @@ require 'spec_helper'
RSpec.describe AlertManagement::AlertAssignee do
describe 'associations' do
it { is_expected.to belong_to(:alert) }
- it { is_expected.to belong_to(:assignee) }
+
+ it do
+ is_expected.to belong_to(:assignee).class_name('User')
+ .with_foreign_key(:user_id).inverse_of(:alert_assignees)
+ end
end
describe 'validations' do
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 685ed81ec84..ff77ca2ab64 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -16,9 +16,13 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to belong_to(:prometheus_alert).optional }
it { is_expected.to belong_to(:environment).optional }
it { is_expected.to have_many(:assignees).through(:alert_assignees) }
- it { is_expected.to have_many(:notes) }
- it { is_expected.to have_many(:ordered_notes) }
- it { is_expected.to have_many(:user_mentions) }
+ it { is_expected.to have_many(:notes).inverse_of(:noteable) }
+ it { is_expected.to have_many(:ordered_notes).class_name('Note').inverse_of(:noteable) }
+
+ it do
+ is_expected.to have_many(:user_mentions).class_name('AlertManagement::AlertUserMention')
+ .with_foreign_key(:alert_management_alert_id).inverse_of(:alert)
+ end
end
describe 'validations' do
diff --git a/spec/models/alert_management/alert_user_mention_spec.rb b/spec/models/alert_management/alert_user_mention_spec.rb
index 27c3d290dde..083bf667bea 100644
--- a/spec/models/alert_management/alert_user_mention_spec.rb
+++ b/spec/models/alert_management/alert_user_mention_spec.rb
@@ -4,7 +4,11 @@ require 'spec_helper'
RSpec.describe AlertManagement::AlertUserMention do
describe 'associations' do
- it { is_expected.to belong_to(:alert_management_alert) }
+ it do
+ is_expected.to belong_to(:alert).class_name('::AlertManagement::Alert')
+ .with_foreign_key(:alert_management_alert_id).inverse_of(:user_mentions)
+ end
+
it { is_expected.to belong_to(:note) }
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 5b99c68ec80..8387f4021b6 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
+RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
using RSpec::Parameterized::TableSyntax
subject(:setting) { described_class.create_from_defaults }
@@ -25,6 +25,20 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { expect(setting.kroki_formats).to eq({}) }
end
+ describe 'associations' do
+ it do
+ is_expected.to belong_to(:self_monitoring_project).class_name('Project')
+ .with_foreign_key(:instance_administration_project_id)
+ .inverse_of(:application_setting)
+ end
+
+ it do
+ is_expected.to belong_to(:instance_group).class_name('Group')
+ .with_foreign_key(:instance_administrators_group_id)
+ .inverse_of(:application_setting)
+ end
+ end
+
describe 'validations' do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
@@ -132,6 +146,9 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value(false).for(:user_defaults_to_private_profile) }
it { is_expected.not_to allow_value(nil).for(:user_defaults_to_private_profile) }
+ it { is_expected.to allow_values([true, false]).for(:deny_all_requests_except_allowed) }
+ it { is_expected.not_to allow_value(nil).for(:deny_all_requests_except_allowed) }
+
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)
@@ -182,7 +199,8 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") }
- %i[notes_create_limit search_rate_limit search_rate_limit_unauthenticated users_get_by_id_limit].each do |setting|
+ %i[notes_create_limit search_rate_limit search_rate_limit_unauthenticated users_get_by_id_limit
+ projects_api_rate_limit_unauthenticated].each do |setting|
it { is_expected.to allow_value(400).for(setting) }
it { is_expected.not_to allow_value('two').for(setting) }
it { is_expected.not_to allow_value(nil).for(setting) }
@@ -209,6 +227,12 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value('disabled').for(:whats_new_variant) }
it { is_expected.not_to allow_value(nil).for(:whats_new_variant) }
+ it { is_expected.to allow_value('http://example.com/').for(:public_runner_releases_url) }
+ it { is_expected.not_to allow_value(nil).for(:public_runner_releases_url) }
+
+ it { is_expected.to allow_value([true, false]).for(:update_runner_versions_enabled) }
+ it { is_expected.not_to allow_value(nil).for(:update_runner_versions_enabled) }
+
it { is_expected.not_to allow_value(['']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(['OBVIOUSLY_WRONG']).for(:valid_runner_registrars) }
it { is_expected.not_to allow_value(%w(project project)).for(:valid_runner_registrars) }
@@ -228,6 +252,10 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value(false).for(:allow_runner_registration_token) }
it { is_expected.not_to allow_value(nil).for(:allow_runner_registration_token) }
+ it { is_expected.to allow_value(true).for(:gitlab_dedicated_instance) }
+ it { is_expected.to allow_value(false).for(:gitlab_dedicated_instance) }
+ it { is_expected.not_to allow_value(nil).for(:gitlab_dedicated_instance) }
+
context 'when deactivate_dormant_users is enabled' do
before do
stub_application_setting(deactivate_dormant_users: true)
@@ -318,7 +346,7 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
- describe 'default_branch_name validaitions' do
+ describe 'default_branch_name validations' do
context "when javascript tags get sanitized properly" do
it "gets sanitized properly" do
setting.update!(default_branch_name: "hello<script>alert(1)</script>")
@@ -585,6 +613,23 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
+ describe 'setting validated as `addressable_url` configured with external URI' do
+ before do
+ # Use any property that has the `addressable_url` validation.
+ setting.help_page_documentation_base_url = 'http://example.com'
+ end
+
+ it 'is valid by default' do
+ expect(setting).to be_valid
+ end
+
+ it 'is invalid when unpersisted `deny_all_requests_except_allowed` property is true' do
+ setting.deny_all_requests_except_allowed = true
+
+ expect(setting).not_to be_valid
+ end
+ end
+
context 'key restrictions' do
it 'does not allow all key types to be disabled' do
Gitlab::SSHPublicKey.supported_types.each do |type|
@@ -1124,6 +1169,11 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
it { is_expected.to allow_value(*Gitlab::I18n.available_locales).for(:default_preferred_language) }
it { is_expected.not_to allow_value(nil, '', 'invalid_locale').for(:default_preferred_language) }
end
+
+ context 'for default_syntax_highlighting_theme' do
+ it { is_expected.to allow_value(*Gitlab::ColorSchemes.valid_ids).for(:default_syntax_highlighting_theme) }
+ it { is_expected.not_to allow_value(nil, 0, Gitlab::ColorSchemes.available_schemes.size + 1).for(:default_syntax_highlighting_theme) }
+ end
end
context 'restrict creating duplicates' do
@@ -1144,6 +1194,17 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
end
end
+ describe 'ADDRESSABLE_URL_VALIDATION_OPTIONS' do
+ it 'is applied to all addressable_url validated properties' do
+ url_validators = described_class.validators.select { |validator| validator.is_a?(AddressableUrlValidator) }
+
+ url_validators.each do |validator|
+ expect(validator.options).to match(hash_including(described_class::ADDRESSABLE_URL_VALIDATION_OPTIONS)),
+ "#{validator.attributes} should use ADDRESSABLE_URL_VALIDATION_OPTIONS"
+ end
+ end
+ end
+
describe '#disabled_oauth_sign_in_sources=' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([:github])
@@ -1474,4 +1535,50 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do
expect(setting.personal_access_tokens_disabled?).to eq(false)
end
end
+
+ describe 'email_confirmation_setting prefixes' do
+ before do
+ described_class.create_from_defaults
+ end
+
+ context 'when feature flag `soft_email_confirmation` is not enabled' do
+ before do
+ stub_feature_flags(soft_email_confirmation: false)
+ end
+
+ where(:email_confirmation_setting, :off, :soft, :hard) do
+ 'off' | true | false | false
+ 'soft' | false | true | false
+ 'hard' | false | false | true
+ end
+
+ with_them do
+ it 'returns the correct value when prefixed' do
+ stub_application_setting_enum('email_confirmation_setting', email_confirmation_setting)
+
+ expect(described_class.last.email_confirmation_setting_off?).to be off
+ expect(described_class.last.email_confirmation_setting_soft?).to be soft
+ expect(described_class.last.email_confirmation_setting_hard?).to be hard
+ end
+ end
+
+ it 'calls super' do
+ expect(described_class.last.email_confirmation_setting_off?).to be true
+ expect(described_class.last.email_confirmation_setting_soft?).to be false
+ expect(described_class.last.email_confirmation_setting_hard?).to be false
+ end
+ end
+
+ context 'when feature flag `soft_email_confirmation` is enabled' do
+ before do
+ stub_feature_flags(soft_email_confirmation: true)
+ end
+
+ it 'returns correct value when enum is prefixed' do
+ expect(described_class.last.email_confirmation_setting_off?).to be false
+ expect(described_class.last.email_confirmation_setting_soft?).to be true
+ expect(described_class.last.email_confirmation_setting_hard?).to be false
+ end
+ end
+ end
end
diff --git a/spec/models/audit_event_spec.rb b/spec/models/audit_event_spec.rb
index 9f2724cebee..9e667836b45 100644
--- a/spec/models/audit_event_spec.rb
+++ b/spec/models/audit_event_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe AuditEvent do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user).with_foreign_key(:author_id).inverse_of(:audit_events) }
+ end
+
describe 'validations' do
include_examples 'validates IP address' do
let(:attribute) { :ip_address }
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
index 6017298e85b..f469dee5ba1 100644
--- a/spec/models/board_spec.rb
+++ b/spec/models/board_spec.rb
@@ -8,7 +8,13 @@ RSpec.describe Board do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) }
+
+ it do
+ is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all)
+ .inverse_of(:board)
+ end
+
+ it { is_expected.to have_many(:destroyable_lists).order(list_type: :asc, position: :asc).inverse_of(:board) }
end
describe 'validations' do
diff --git a/spec/models/bulk_import_spec.rb b/spec/models/bulk_import_spec.rb
index 3430da43f62..acb1f4a2ef7 100644
--- a/spec/models/bulk_import_spec.rb
+++ b/spec/models/bulk_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImport, type: :model do
+RSpec.describe BulkImport, type: :model, feature_category: :importers do
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
let_it_be(:finished_bulk_import) { create(:bulk_import, :finished) }
@@ -48,4 +48,31 @@ RSpec.describe BulkImport, type: :model do
expect(bulk_import.source_version_info.to_s).to eq(bulk_import.source_version)
end
end
+
+ describe '#update_has_failures' do
+ let(:import) { create(:bulk_import, :started) }
+ let(:entity) { create(:bulk_import_entity, bulk_import: import) }
+
+ context 'when entity has failures' do
+ it 'sets has_failures flag to true' do
+ expect(import.has_failures).to eq(false)
+
+ entity.update!(has_failures: true)
+ import.fail_op!
+
+ expect(import.has_failures).to eq(true)
+ end
+ end
+
+ context 'when entity does not have failures' do
+ it 'sets has_failures flag to false' do
+ expect(import.has_failures).to eq(false)
+
+ entity.update!(has_failures: false)
+ import.fail_op!
+
+ expect(import.has_failures).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/batch_tracker_spec.rb b/spec/models/bulk_imports/batch_tracker_spec.rb
new file mode 100644
index 00000000000..336943228c7
--- /dev/null
+++ b/spec/models/bulk_imports/batch_tracker_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::BatchTracker, type: :model, feature_category: :importers do
+ describe 'associations' do
+ it { is_expected.to belong_to(:tracker) }
+ end
+
+ describe 'validations' do
+ subject { build(:bulk_import_batch_tracker) }
+
+ it { is_expected.to validate_presence_of(:batch_number) }
+ it { is_expected.to validate_uniqueness_of(:batch_number).scoped_to(:tracker_id) }
+ end
+end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 56796aa1fe4..45f120e6773 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -6,8 +6,13 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
describe 'associations' do
it { is_expected.to belong_to(:bulk_import).required }
it { is_expected.to belong_to(:parent) }
- it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:group).optional.with_foreign_key(:namespace_id).inverse_of(:bulk_import_entities) }
it { is_expected.to belong_to(:project) }
+
+ it do
+ is_expected.to have_many(:trackers).class_name('BulkImports::Tracker')
+ .with_foreign_key(:bulk_import_entity_id).inverse_of(:entity)
+ end
end
describe 'validations' do
@@ -93,8 +98,6 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
it 'is invalid as a project_entity' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, :project_entity, group: build(:group), project: nil)
expect(entity).not_to be_valid
@@ -104,8 +107,6 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
context 'when associated with a project and no group' do
it 'is valid' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, :project_entity, group: nil, project: build(:project))
expect(entity).to be_valid
@@ -135,8 +136,6 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
context 'when the parent is a project import' do
it 'is invalid' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, parent: build(:bulk_import_entity, :project_entity))
expect(entity).not_to be_valid
@@ -178,38 +177,13 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
end
- context 'when bulk_import_projects feature flag is disabled and source_type is a project_entity' do
- it 'is invalid' do
- stub_feature_flags(bulk_import_projects: false)
-
- entity = build(:bulk_import_entity, :project_entity)
-
- expect(entity).not_to be_valid
- expect(entity.errors[:base]).to include('invalid entity source type')
- end
- end
-
- context 'when bulk_import_projects feature flag is enabled and source_type is a project_entity' do
+ context 'when source_type is a project_entity' do
it 'is valid' do
- stub_feature_flags(bulk_import_projects: true)
-
entity = build(:bulk_import_entity, :project_entity)
expect(entity).to be_valid
end
end
-
- context 'when bulk_import_projects feature flag is enabled on root ancestor level and source_type is a project_entity' do
- it 'is valid' do
- top_level_namespace = create(:group)
-
- stub_feature_flags(bulk_import_projects: top_level_namespace)
-
- entity = build(:bulk_import_entity, :project_entity, destination_namespace: top_level_namespace.full_path)
-
- expect(entity).to be_valid
- end
- end
end
describe '#encoded_source_full_path' do
@@ -412,4 +386,30 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d
end
end
end
+
+ describe '#update_has_failures' do
+ let(:entity) { create(:bulk_import_entity) }
+
+ context 'when entity has failures' do
+ it 'sets has_failures flag to true' do
+ expect(entity.has_failures).to eq(false)
+
+ create(:bulk_import_failure, entity: entity)
+
+ entity.fail_op!
+
+ expect(entity.has_failures).to eq(true)
+ end
+ end
+
+ context 'when entity does not have failures' do
+ it 'sets has_failures flag to false' do
+ expect(entity.has_failures).to eq(false)
+
+ entity.fail_op!
+
+ expect(entity.has_failures).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/bulk_imports/export_batch_spec.rb b/spec/models/bulk_imports/export_batch_spec.rb
new file mode 100644
index 00000000000..43209921b9c
--- /dev/null
+++ b/spec/models/bulk_imports/export_batch_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::ExportBatch, type: :model, feature_category: :importers do
+ describe 'associations' do
+ it { is_expected.to belong_to(:export) }
+ it { is_expected.to have_one(:upload) }
+ end
+
+ describe 'validations' do
+ subject { build(:bulk_import_export_batch) }
+
+ it { is_expected.to validate_presence_of(:batch_number) }
+ it { is_expected.to validate_uniqueness_of(:batch_number).scoped_to(:export_id) }
+ end
+end
diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb
index d85b77d599b..7173d032bc2 100644
--- a/spec/models/bulk_imports/export_spec.rb
+++ b/spec/models/bulk_imports/export_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe BulkImports::Export, type: :model do
+RSpec.describe BulkImports::Export, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:upload) }
+ it { is_expected.to have_many(:batches) }
end
describe 'validations' do
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 0f02c5c546f..21fe6cfb3fa 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -42,6 +42,18 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
it 'returns relation tree of a top level relation' do
expect(subject.top_relation_tree('labels')).to eq('priorities' => {})
end
+
+ it 'returns relation tree with merged with deprecated tree' do
+ expect(subject.top_relation_tree('ci_pipelines')).to match(
+ a_hash_including(
+ {
+ 'external_pull_request' => {},
+ 'merge_request' => {},
+ 'stages' => { 'bridges' => {}, 'builds' => {}, 'generic_commit_statuses' => {}, 'statuses' => {} }
+ }
+ )
+ )
+ end
end
describe '#relation_excluded_keys' do
diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb
index 1516ab106cb..a618a12df6b 100644
--- a/spec/models/bulk_imports/tracker_spec.rb
+++ b/spec/models/bulk_imports/tracker_spec.rb
@@ -4,7 +4,10 @@ require 'spec_helper'
RSpec.describe BulkImports::Tracker, type: :model do
describe 'associations' do
- it { is_expected.to belong_to(:entity).required }
+ it do
+ is_expected.to belong_to(:entity).required.class_name('BulkImports::Entity')
+ .with_foreign_key(:bulk_import_entity_id).inverse_of(:trackers)
+ end
end
describe 'validations' do
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 0838c232872..9d6b1a56458 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe ChatName, feature_category: :integrations do
subject { chat_name }
- it { is_expected.to belong_to(:integration) }
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_presence_of(:user) }
@@ -16,12 +15,6 @@ RSpec.describe ChatName, feature_category: :integrations do
it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:team_id) }
- it 'is not removed when the project is deleted' do
- expect { subject.reload.integration.project.delete }.not_to change { ChatName.count }
-
- expect(ChatName.where(id: subject.id)).to exist
- end
-
describe '#update_last_used_at', :clean_gitlab_redis_shared_state do
it 'updates the last_used_at timestamp' do
expect(subject.last_used_at).to be_nil
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index fb50ba89cd3..c3b445cbbe5 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -22,7 +22,6 @@ RSpec.describe Ci::BuildMetadata do
it { is_expected.to belong_to(:build) }
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:runner_machine) }
describe '#update_timeout_state' do
subject { metadata }
diff --git a/spec/models/ci/build_pending_state_spec.rb b/spec/models/ci/build_pending_state_spec.rb
index bff0b35f878..c978e3dba36 100644
--- a/spec/models/ci/build_pending_state_spec.rb
+++ b/spec/models/ci/build_pending_state_spec.rb
@@ -3,10 +3,15 @@
require 'spec_helper'
RSpec.describe Ci::BuildPendingState, feature_category: :continuous_integration do
+ describe 'associations' do
+ it do
+ is_expected.to belong_to(:build).class_name('Ci::Build').with_foreign_key(:build_id).inverse_of(:pending_state)
+ end
+ end
+
describe 'validations' do
subject(:pending_state) { build(:ci_build_pending_state) }
- it { is_expected.to belong_to(:build) }
it { is_expected.to validate_presence_of(:build) }
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 80d6693e08e..ca7f4794a0c 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -22,20 +22,35 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
+ it { is_expected.to belong_to(:pipeline).inverse_of(:builds) }
it { is_expected.to have_many(:needs).with_foreign_key(:build_id) }
- it { is_expected.to have_many(:sourced_pipelines).with_foreign_key(:source_job_id) }
- it { is_expected.to have_one(:sourced_pipeline).with_foreign_key(:source_job_id) }
+
+ it do
+ is_expected.to have_many(:sourced_pipelines).class_name('Ci::Sources::Pipeline').with_foreign_key(:source_job_id)
+ .inverse_of(:build)
+ end
+
it { is_expected.to have_many(:job_variables).with_foreign_key(:job_id) }
it { is_expected.to have_many(:report_results).with_foreign_key(:build_id) }
it { is_expected.to have_many(:pages_deployments).with_foreign_key(:ci_build_id) }
it { is_expected.to have_one(:deployment) }
- it { is_expected.to have_one(:runner_machine).through(:metadata) }
+ it { is_expected.to have_one(:runner_machine).through(:runner_machine_build) }
it { is_expected.to have_one(:runner_session).with_foreign_key(:build_id) }
it { is_expected.to have_one(:trace_metadata).with_foreign_key(:build_id) }
it { is_expected.to have_one(:runtime_metadata).with_foreign_key(:build_id) }
- it { is_expected.to have_one(:pending_state).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:pending_state).with_foreign_key(:build_id).inverse_of(:build) }
+
+ it do
+ is_expected.to have_one(:queuing_entry).class_name('Ci::PendingBuild').with_foreign_key(:build_id).inverse_of(:build)
+ end
+
+ it do
+ is_expected.to have_one(:runtime_metadata).class_name('Ci::RunningBuild').with_foreign_key(:build_id)
+ .inverse_of(:build)
+ end
+
it { is_expected.to have_many(:terraform_state_versions).inverse_of(:build).with_foreign_key(:ci_build_id) }
it { is_expected.to validate_presence_of(:ref) }
@@ -1040,7 +1055,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'non public artifacts' do
- let(:build) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it { is_expected.to be_falsey }
end
@@ -1422,11 +1437,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:build) { create(:ci_build, trait, pipeline: pipeline) }
let(:event) { state }
- context "when transitioning to #{params[:state]}" do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context "when transitioning to #{params[:state]}", :saas do
it 'increments build_completed_report_type metric' do
expect(
::Gitlab::Ci::Artifacts::Metrics
@@ -1926,62 +1937,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#failed_but_allowed?' do
- subject { build.failed_but_allowed? }
-
- context 'when build is not allowed to fail' do
- before do
- build.allow_failure = false
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when build is allowed to fail' do
- before do
- build.allow_failure = true
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when build is a manual action' do
- before do
- build.status = 'manual'
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
describe 'flags' do
describe '#cancelable?' do
subject { build }
@@ -3639,6 +3594,46 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
+ context 'for the google_play integration' do
+ let_it_be(:google_play_integration) { create(:google_play_integration) }
+
+ let(:google_play_variables) do
+ [
+ { key: 'SUPPLY_JSON_KEY_DATA', value: google_play_integration.service_account_key, masked: true, public: false }
+ ]
+ end
+
+ context 'when the google_play integration exists' do
+ context 'when a build is protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(true)
+ build.project.update!(google_play_integration: google_play_integration)
+ end
+
+ it 'includes google_play variables' do
+ is_expected.to include(*google_play_variables)
+ end
+ end
+
+ context 'when a build is not protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(false)
+ build.project.update!(google_play_integration: google_play_integration)
+ end
+
+ it 'does not include the google_play variable' do
+ expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false)
+ end
+ end
+ end
+
+ context 'when the googel_play integration does not exist' do
+ it 'does not include google_play variable' do
+ expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false)
+ end
+ end
+ end
+
context 'when build has dependency which has dotenv variable' do
let!(:prepare) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: [prepare.name] }) }
@@ -5784,12 +5779,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(build.token).to be_nil
expect(build.changes).to be_empty
end
-
- it 'does not remove the token when FF is disabled' do
- stub_feature_flags(remove_job_token_on_completion: false)
-
- expect { build.remove_token! }.not_to change(build, :token)
- end
end
describe 'metadata partitioning', :ci_partitioning do
@@ -5905,4 +5894,31 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
end
+
+ describe 'token format for builds transiting into pending' do
+ let(:partition_id) { 100 }
+ let(:ci_build) { described_class.new(partition_id: partition_id) }
+
+ context 'when build is initialized without a token and transits to pending' do
+ let(:partition_id_prefix_in_16_bit_encode) { partition_id.to_s(16) + '_' }
+
+ it 'generates a token' do
+ expect { ci_build.enqueue }
+ .to change { ci_build.token }.from(nil).to(a_string_starting_with(partition_id_prefix_in_16_bit_encode))
+ end
+ end
+
+ context 'when build is initialized with a token and transits to pending' do
+ let(:token) { 'an_existing_secret_token' }
+
+ before do
+ ci_build.set_token(token)
+ end
+
+ it 'does not change the existing token' do
+ expect { ci_build.enqueue }
+ .not_to change { ci_build.token }.from(token)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index ac0a18a176d..f338c2727ad 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -20,6 +20,12 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
stub_artifacts_object_storage
end
+ describe 'associations' do
+ it do
+ is_expected.to belong_to(:build).class_name('Ci::Build').with_foreign_key(:build_id).inverse_of(:trace_chunks)
+ end
+ end
+
it_behaves_like 'having unique enum values'
def redis_instance
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
new file mode 100644
index 00000000000..c9ccecbc9fe
--- /dev/null
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:project_1) { create(:project, namespace: namespace) }
+ let_it_be(:project_2) { create(:project, namespace: namespace) }
+ let_it_be(:project_3) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:list) { described_class.new(namespace, user) }
+
+ describe '#new' do
+ context 'when namespace is not a root namespace' do
+ let(:namespace) { create(:group, :nested) }
+
+ it 'raises an exception' do
+ expect { list }.to raise_error(ArgumentError, 'Namespace is not a root namespace')
+ end
+ end
+ end
+
+ describe '#resources' do
+ subject(:resources) { list.resources }
+
+ context 'when the user has access to all projects in the namespace' do
+ before do
+ namespace.add_developer(user)
+ end
+
+ context 'when the namespace has no catalog resources' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the namespace has catalog resources' do
+ let!(:resource) { create(:catalog_resource, project: project_1) }
+ let!(:other_namespace_resource) { create(:catalog_resource, project: project_3) }
+
+ it 'contains only catalog resources for projects in that namespace' do
+ is_expected.to contain_exactly(resource)
+ end
+ end
+ end
+
+ context 'when the user only has access to some projects in the namespace' do
+ let!(:resource_1) { create(:catalog_resource, project: project_1) }
+ let!(:resource_2) { create(:catalog_resource, project: project_2) }
+
+ before do
+ project_1.add_developer(user)
+ end
+
+ it 'only returns catalog resources for projects the user has access to' do
+ is_expected.to contain_exactly(resource_1)
+ end
+ end
+
+ context 'when the user does not have access to the namespace' do
+ let!(:resource) { create(:catalog_resource, project: project_1) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index cd55817243f..6f73d89d760 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -6,7 +6,11 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
let(:daily_build_group_report_result) { build(:ci_daily_build_group_report_result) }
describe 'associations' do
- it { is_expected.to belong_to(:last_pipeline) }
+ it do
+ is_expected.to belong_to(:last_pipeline).class_name('Ci::Pipeline')
+ .with_foreign_key(:last_pipeline_id).inverse_of(:daily_build_group_report_results)
+ end
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:group) }
end
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index e73319cfcd7..f8f184c63a1 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::GroupVariable, feature_category: :pipeline_authoring do
+RSpec.describe Ci::GroupVariable, feature_category: :pipeline_composition do
let_it_be_with_refind(:group) { create(:group) }
subject { build(:ci_group_variable, group: group) }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index e94445f17cd..f7fafd93f4b 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
describe "Associations" do
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:job) }
+ it { is_expected.to belong_to(:job).class_name('Ci::Build').with_foreign_key(:job_id).inverse_of(:job_artifacts) }
it { is_expected.to validate_presence_of(:job) }
it { is_expected.to validate_presence_of(:partition_id) }
end
@@ -243,6 +243,29 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
end
end
+ describe '.non_trace' do
+ subject { described_class.non_trace }
+
+ context 'when there is only a trace job artifact' do
+ let!(:trace) { create(:ci_job_artifact, :trace) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when there is only a non-trace job artifact' do
+ let!(:junit) { create(:ci_job_artifact, :junit) }
+
+ it { is_expected.to eq([junit]) }
+ end
+
+ context 'when there are both trace and non-trace job artifacts' do
+ let!(:trace) { create(:ci_job_artifact, :trace) }
+ let!(:junit) { create(:ci_job_artifact, :junit) }
+
+ it { is_expected.to eq([junit]) }
+ end
+ end
+
describe '.downloadable' do
subject { described_class.downloadable }
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 9ae061a3702..51f0f4878e7 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -160,13 +160,5 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
include_examples 'enforces outbound scope only'
end
-
- context 'when inbound scope flag disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- include_examples 'enforces outbound scope only'
- end
end
end
diff --git a/spec/models/ci/job_variable_spec.rb b/spec/models/ci/job_variable_spec.rb
index 0a65708160a..a56e6b6be43 100644
--- a/spec/models/ci/job_variable_spec.rb
+++ b/spec/models/ci/job_variable_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Ci::JobVariable, feature_category: :continuous_integration do
describe 'associations' do
let!(:job_variable) { create(:ci_job_variable) }
- it { is_expected.to belong_to(:job) }
+ it { is_expected.to belong_to(:job).class_name('Ci::Build').with_foreign_key(:job_id).inverse_of(:job_variables) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) }
end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 9b70f7c2839..c441be58edf 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:owner) }
- it { is_expected.to have_many(:pipelines) }
+ it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:variables) }
it { is_expected.to respond_to(:ref) }
@@ -281,4 +281,19 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
let!(:model) { create(:ci_pipeline_schedule, project: parent) }
end
end
+
+ describe 'before_destroy' do
+ let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: ' 0 0 * * * ') }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
+
+ it 'nullifys associated pipelines' do
+ expect(pipeline_schedule).to receive(:nullify_dependent_associations_in_batches).and_call_original
+
+ result = pipeline_schedule.destroy
+
+ expect(result).to be_truthy
+ expect(pipeline.reload.pipeline_schedule).to be_nil
+ expect(described_class.find_by(id: pipeline_schedule.id)).to be_nil
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 61422978df7..d50672da8e5 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -19,32 +19,67 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:auto_canceled_by) }
+ it { is_expected.to belong_to(:auto_canceled_by).class_name('Ci::Pipeline').inverse_of(:auto_canceled_pipelines) }
it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:external_pull_request) }
it { is_expected.to have_many(:statuses) }
- it { is_expected.to have_many(:trigger_requests) }
+ it { is_expected.to have_many(:trigger_requests).with_foreign_key(:commit_id).inverse_of(:pipeline) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:builds) }
- it { is_expected.to have_many(:statuses_order_id_desc) }
+
+ it do
+ is_expected.to have_many(:statuses_order_id_desc)
+ .class_name('CommitStatus').with_foreign_key(:commit_id).inverse_of(:pipeline)
+ end
+
it { is_expected.to have_many(:bridges) }
it { is_expected.to have_many(:job_artifacts).through(:builds) }
it { is_expected.to have_many(:build_trace_chunks).through(:builds) }
- it { is_expected.to have_many(:auto_canceled_pipelines) }
- it { is_expected.to have_many(:auto_canceled_jobs) }
- it { is_expected.to have_many(:sourced_pipelines) }
+
it { is_expected.to have_many(:triggered_pipelines) }
it { is_expected.to have_many(:pipeline_artifacts) }
- it { is_expected.to have_one(:chat_data) }
+ it do
+ is_expected.to have_many(:failed_builds).class_name('Ci::Build')
+ .with_foreign_key(:commit_id).inverse_of(:pipeline)
+ end
+
+ it do
+ is_expected.to have_many(:cancelable_statuses).class_name('CommitStatus')
+ .with_foreign_key(:commit_id).inverse_of(:pipeline)
+ end
+
+ it do
+ is_expected.to have_many(:auto_canceled_pipelines).class_name('Ci::Pipeline')
+ .with_foreign_key(:auto_canceled_by_id).inverse_of(:auto_canceled_by)
+ end
+
+ it do
+ is_expected.to have_many(:auto_canceled_jobs).class_name('CommitStatus')
+ .with_foreign_key(:auto_canceled_by_id).inverse_of(:auto_canceled_by)
+ end
+
+ it do
+ is_expected.to have_many(:sourced_pipelines).class_name('Ci::Sources::Pipeline')
+ .with_foreign_key(:source_pipeline_id).inverse_of(:source_pipeline)
+ end
+
it { is_expected.to have_one(:source_pipeline) }
+ it { is_expected.to have_one(:chat_data) }
it { is_expected.to have_one(:triggered_by_pipeline) }
it { is_expected.to have_one(:source_job) }
it { is_expected.to have_one(:pipeline_config) }
it { is_expected.to have_one(:pipeline_metadata) }
+ it do
+ is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult')
+ .with_foreign_key(:last_pipeline_id).inverse_of(:last_pipeline)
+ end
+
+ it { is_expected.to have_many(:latest_builds_report_results).through(:latest_builds).source(:report_results) }
+
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :git_author_full_text }
@@ -872,6 +907,26 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
expect(pipeline).to be_valid
end
end
+
+ context 'when source is unknown' do
+ subject(:pipeline) { create(:ci_empty_pipeline, :created) }
+
+ let(:attr) { :source }
+ let(:attr_value) { :unknown }
+
+ it_behaves_like 'having enum with nil value'
+ end
+ end
+
+ describe '#config_source' do
+ context 'when source is unknown' do
+ subject(:pipeline) { create(:ci_empty_pipeline, :created) }
+
+ let(:attr) { :config_source }
+ let(:attr_value) { :unknown_source }
+
+ it_behaves_like 'having enum with nil value'
+ end
end
describe '#block' do
@@ -1661,6 +1716,154 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe 'merge status subscription trigger' do
+ shared_examples 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated' do
+ context 'when state transitions to running' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.run }
+ end
+ end
+
+ context 'when state transitions to success' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.succeed }
+ end
+ end
+
+ context 'when state transitions to failed' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.drop }
+ end
+ end
+
+ context 'when state transitions to canceled' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.cancel }
+ end
+ end
+
+ context 'when state transitions to skipped' do
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.skip }
+ end
+ end
+ end
+
+ shared_examples 'state transition triggering GraphQL subscription mergeRequestMergeStatusUpdated' do
+ context 'when state transitions to running' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.run }
+ end
+ end
+
+ context 'when state transitions to success' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.succeed }
+ end
+ end
+
+ context 'when state transitions to failed' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.drop }
+ end
+ end
+
+ context 'when state transitions to canceled' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.cancel }
+ end
+ end
+
+ context 'when state transitions to skipped' do
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { pipeline.skip }
+ end
+ end
+
+ context 'when only_allow_merge_if_pipeline_succeeds? returns false' do
+ let(:only_allow_merge_if_pipeline_succeeds?) { false }
+
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+
+ context 'when pipeline_trigger_merge_status feature flag is disabled' do
+ before do
+ stub_feature_flags(pipeline_trigger_merge_status: false)
+ end
+
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
+ context 'when pipeline has merge requests' do
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :simple,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ let(:only_allow_merge_if_pipeline_succeeds?) { true }
+
+ before do
+ allow(project)
+ .to receive(:only_allow_merge_if_pipeline_succeeds?)
+ .and_return(only_allow_merge_if_pipeline_succeeds?)
+ end
+
+ context 'when for a specific merge request' do
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ merge_request: merge_request
+ )
+ end
+
+ it_behaves_like 'state transition triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+
+ context 'when pipeline is a child' do
+ let(:parent_pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ merge_request: merge_request
+ )
+ end
+
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ child_of: parent_pipeline,
+ merge_request: merge_request
+ )
+ end
+
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
+ context 'when for merge requests matching the source branch and SHA' do
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
+ end
+
+ it_behaves_like 'state transition triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
+ context 'when pipeline has no merge requests' do
+ it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated'
+ end
+ end
+
def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
create(:ci_build, *traits,
name: name,
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index db22d8f3a6c..cf2c176816d 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -83,7 +83,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
- metadata runner_machine_id runner_machine runner_session trace_chunks upstream_pipeline_id
+ metadata runner_machine_build runner_machine runner_session trace_chunks upstream_pipeline_id
artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
diff --git a/spec/models/ci/runner_machine_build_spec.rb b/spec/models/ci/runner_machine_build_spec.rb
new file mode 100644
index 00000000000..b43ff535477
--- /dev/null
+++ b/spec/models/ci/runner_machine_build_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerMachineBuild, model: true, feature_category: :runner_fleet do
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+ let_it_be(:build) { create(:ci_build, runner_machine: runner_machine) }
+
+ it { is_expected.to belong_to(:build) }
+ it { is_expected.to belong_to(:runner_machine) }
+
+ describe 'partitioning' do
+ context 'with build' do
+ let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) }
+ let(:runner_machine_build) { FactoryBot.build(:ci_runner_machine_build, build: build) }
+
+ it 'sets partition_id to the current partition value' do
+ expect { runner_machine_build.valid? }.to change { runner_machine_build.partition_id }
+ .to(ci_testing_partition_id)
+ end
+
+ context 'when it is already set' do
+ let(:runner_machine_build) { FactoryBot.build(:ci_runner_machine_build, partition_id: 125) }
+
+ it 'does not change the partition_id value' do
+ expect { runner_machine_build.valid? }.not_to change { runner_machine_build.partition_id }
+ end
+ end
+ end
+
+ context 'without build' do
+ let(:runner_machine_build) { FactoryBot.build(:ci_runner_machine_build, build: nil) }
+
+ it { is_expected.to validate_presence_of(:partition_id) }
+
+ it 'does not change the partition_id value' do
+ expect { runner_machine_build.valid? }.not_to change { runner_machine_build.partition_id }
+ end
+ end
+ end
+
+ describe 'ci_sliding_list partitioning' do
+ let(:connection) { described_class.connection }
+ let(:partition_manager) { Gitlab::Database::Partitioning::PartitionManager.new(described_class) }
+
+ let(:partitioning_strategy) { described_class.partitioning_strategy }
+
+ it { expect(partitioning_strategy.missing_partitions).to be_empty }
+ it { expect(partitioning_strategy.extra_partitions).to be_empty }
+ it { expect(partitioning_strategy.current_partitions).to include partitioning_strategy.initial_partition }
+ it { expect(partitioning_strategy.active_partition).to be_present }
+ end
+
+ context 'loose foreign key on p_ci_runner_machine_builds.runner_machine_id' do # rubocop:disable RSpec/ContextWording
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:ci_runner_machine) }
+ let!(:model) { create(:ci_runner_machine_build, runner_machine: parent) }
+ end
+ end
+
+ describe '.for_build' do
+ subject(:for_build) { described_class.for_build(build_id) }
+
+ context 'with valid build_id' do
+ let(:build_id) { build.id }
+
+ it { is_expected.to contain_exactly(described_class.find_by_build_id(build_id)) }
+ end
+
+ context 'with valid build_ids' do
+ let(:build2) { create(:ci_build, runner_machine: runner_machine) }
+ let(:build_id) { [build, build2] }
+
+ it { is_expected.to eq(described_class.where(build_id: build_id)) }
+ end
+
+ context 'with non-existeng build_id' do
+ let(:build_id) { non_existing_record_id }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '.pluck_runner_machine_id_and_build_id' do
+ subject { scope.pluck_build_id_and_runner_machine_id }
+
+ context 'with default scope' do
+ let(:scope) { described_class }
+
+ it { is_expected.to eq({ build.id => runner_machine.id }) }
+ end
+
+ context 'with scope excluding build' do
+ let(:scope) { described_class.where(build_id: non_existing_record_id) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/models/ci/runner_machine_spec.rb b/spec/models/ci/runner_machine_spec.rb
index d0979d8a485..0989477cd21 100644
--- a/spec/models/ci/runner_machine_spec.rb
+++ b/spec/models/ci/runner_machine_spec.rb
@@ -5,10 +5,14 @@ require 'spec_helper'
RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'it has loose foreign keys' do
+ let(:factory_name) { :ci_runner_machine }
+ end
+
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
- it { is_expected.to have_many(:build_metadata) }
- it { is_expected.to have_many(:builds).through(:build_metadata) }
+ it { is_expected.to have_many(:runner_machine_builds) }
+ it { is_expected.to have_many(:builds).through(:runner_machine_builds) }
describe 'validation' do
it { is_expected.to validate_presence_of(:runner) }
@@ -53,10 +57,67 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
end
end
+ describe '.online_contact_time_deadline', :freeze_time do
+ subject { described_class.online_contact_time_deadline }
+
+ it { is_expected.to eq(2.hours.ago) }
+ end
+
+ describe '.stale_deadline', :freeze_time do
+ subject { described_class.stale_deadline }
+
+ it { is_expected.to eq(7.days.ago) }
+ end
+
+ describe '#status', :freeze_time do
+ let(:runner_machine) { build(:ci_runner_machine, created_at: 8.days.ago) }
+
+ subject { runner_machine.status }
+
+ context 'if never connected' do
+ before do
+ runner_machine.contacted_at = nil
+ end
+
+ it { is_expected.to eq(:stale) }
+
+ context 'if created recently' do
+ before do
+ runner_machine.created_at = 1.day.ago
+ end
+
+ it { is_expected.to eq(:never_contacted) }
+ end
+ end
+
+ context 'if contacted 1s ago' do
+ before do
+ runner_machine.contacted_at = 1.second.ago
+ end
+
+ it { is_expected.to eq(:online) }
+ end
+
+ context 'if contacted recently' do
+ before do
+ runner_machine.contacted_at = 2.hours.ago
+ end
+
+ it { is_expected.to eq(:offline) }
+ end
+
+ context 'if contacted long time ago' do
+ before do
+ runner_machine.contacted_at = 7.days.ago
+ end
+
+ it { is_expected.to eq(:stale) }
+ end
+ end
+
describe '#heartbeat', :freeze_time do
- let(:runner_machine) { create(:ci_runner_machine) }
+ let(:runner_machine) { create(:ci_runner_machine, version: '15.0.0') }
let(:executor) { 'shell' }
- let(:version) { '15.0.1' }
let(:values) do
{
ip_address: '8.8.8.8',
@@ -76,18 +137,38 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
runner_machine.contacted_at = Time.current
end
- it 'schedules version update' do
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
- heartbeat
+ before do
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
+ end
- expect(runner_machine.runner_version).to be_nil
- end
+ it 'schedules version information update' do
+ heartbeat
- it 'updates cache' do
- expect_redis_update
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
+ end
- heartbeat
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.runner_version).to be_nil
+ end
+
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'does not schedule version information update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
end
context 'with only ip_address specified' do
@@ -96,14 +177,21 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
end
it 'updates only ip_address' do
- attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
+ expect_redis_update(values.merge(contacted_at: Time.current))
+
+ heartbeat
+ end
+
+ context 'with new version having been cached' do
+ let(:version) { '15.0.1' }
- Gitlab::Redis::Cache.with do |redis|
- redis_key = runner_machine.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, attrs, any_args)
+ before do
+ runner_machine.cache_attributes(version: version)
end
- heartbeat
+ it 'does not lose cached version value' do
+ expect { heartbeat }.not_to change { runner_machine.version }.from(version)
+ end
end
end
end
@@ -112,17 +200,29 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
before do
runner_machine.contacted_at = 2.hours.ago
- allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
end
- context 'with invalid runner_machine' do
- before do
- runner_machine.runner = nil
- end
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
- it 'still updates redis cache and database' do
- expect(runner_machine).to be_invalid
+ context 'with invalid runner_machine' do
+ before do
+ runner_machine.runner = nil
+ end
+ it 'still updates redis cache and database' do
+ expect(runner_machine).to be_invalid
+
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+ end
+
+ it 'updates redis cache and database' do
expect_redis_update
does_db_update
@@ -132,58 +232,52 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
end
context 'with unchanged runner_machine version' do
- let(:runner_machine) { create(:ci_runner_machine, version: version) }
+ let(:version) { runner_machine.version }
it 'does not schedule ci_runner_versions update' do
heartbeat
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
end
- end
-
- it 'updates redis cache and database' do
- expect_redis_update
- does_db_update
-
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
- .with(version).once
- end
- Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
- context "with #{executor} executor" do
- let(:executor) { executor }
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
- it 'updates with expected executor type' do
- expect_redis_update
+ it 'updates with expected executor type' do
+ expect_redis_update
- heartbeat
+ heartbeat
- expect(runner_machine.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
- end
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
- def expected_executor_type
- executor.gsub(/[+-]/, '_')
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
end
end
- end
- context "with an unknown executor type" do
- let(:executor) { 'some-unknown-type' }
+ context 'with an unknown executor type' do
+ let(:executor) { 'some-unknown-type' }
- it 'updates with unknown executor type' do
- expect_redis_update
+ it 'updates with unknown executor type' do
+ expect_redis_update
- heartbeat
+ heartbeat
- expect(runner_machine.reload.read_attribute(:executor_type)).to eq('unknown')
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq('unknown')
+ end
end
end
end
- def expect_redis_update
+ def expect_redis_update(values = anything)
+ values_json = values == anything ? anything : Gitlab::Json.dump(values)
+
Gitlab::Redis::Cache.with do |redis|
redis_key = runner_machine.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, anything, any_args).and_call_original
+ expect(redis).to receive(:set).with(redis_key, values_json, any_args).and_call_original
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 01d5fe7f90b..fe49b2c2c7f 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -541,9 +541,9 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
describe '.stale', :freeze_time do
subject { described_class.stale }
- let!(:runner1) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago + 10.seconds) }
- let!(:runner2) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago - 1.second) }
- let!(:runner3) { create(:ci_runner, :instance, created_at: 3.months.ago - 1.second, contacted_at: nil) }
+ let!(:runner1) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago + 1.second) }
+ let!(:runner2) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago) }
+ let!(:runner3) { create(:ci_runner, :instance, created_at: 3.months.ago, contacted_at: nil) }
let!(:runner4) { create(:ci_runner, :instance, created_at: 2.months.ago, contacted_at: nil) }
it 'returns stale runners' do
@@ -551,7 +551,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#stale?', :clean_gitlab_redis_cache do
+ describe '#stale?', :clean_gitlab_redis_cache, :freeze_time do
let(:runner) { build(:ci_runner, :instance) }
subject { runner.stale? }
@@ -570,11 +570,11 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
using RSpec::Parameterized::TableSyntax
where(:created_at, :contacted_at, :expected_stale?) do
- nil | nil | false
- 3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true
- 3.months.ago - 1.second | 3.months.ago + 1.hour | false
- 3.months.ago - 1.second | nil | true
- 3.months.ago + 1.hour | nil | false
+ nil | nil | false
+ 3.months.ago | 3.months.ago | true
+ 3.months.ago | (3.months - 1.hour).ago | false
+ 3.months.ago | nil | true
+ (3.months - 1.hour).ago | nil | false
end
with_them do
@@ -588,9 +588,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
runner.contacted_at = contacted_at
end
- specify do
- is_expected.to eq(expected_stale?)
- end
+ it { is_expected.to eq(expected_stale?) }
end
context 'with cache value' do
@@ -599,9 +597,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
stub_redis_runner_contacted_at(contacted_at.to_s)
end
- specify do
- is_expected.to eq(expected_stale?)
- end
+ it { is_expected.to eq(expected_stale?) }
end
def stub_redis_runner_contacted_at(value)
@@ -617,7 +613,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '.online' do
+ describe '.online', :freeze_time do
subject { described_class.online }
let!(:runner1) { create(:ci_runner, :instance, contacted_at: 2.hours.ago) }
@@ -626,7 +622,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
it { is_expected.to match_array([runner2]) }
end
- describe '#online?', :clean_gitlab_redis_cache do
+ describe '#online?', :clean_gitlab_redis_cache, :freeze_time do
let(:runner) { build(:ci_runner, :instance) }
subject { runner.online? }
@@ -891,8 +887,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#status' do
- let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) }
+ describe '#status', :freeze_time do
+ let(:runner) { build(:ci_runner, :instance, created_at: 3.months.ago) }
let(:legacy_mode) {}
subject { runner.status(legacy_mode) }
@@ -948,7 +944,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted recently' do
before do
- runner.contacted_at = (3.months - 1.hour).ago
+ runner.contacted_at = (3.months - 1.second).ago
end
it { is_expected.to eq(:offline) }
@@ -956,7 +952,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted long time ago' do
before do
- runner.contacted_at = (3.months + 1.second).ago
+ runner.contacted_at = 3.months.ago
end
context 'with legacy_mode enabled' do
@@ -971,7 +967,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#deprecated_rest_status' do
+ describe '#deprecated_rest_status', :freeze_time do
let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
subject { runner.deprecated_rest_status }
@@ -994,8 +990,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted long time ago' do
before do
- runner.created_at = 1.year.ago
- runner.contacted_at = 1.year.ago
+ runner.created_at = 3.months.ago
+ runner.contacted_at = 3.months.ago
end
it { is_expected.to eq(:stale) }
@@ -1076,13 +1072,13 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
- describe '#heartbeat' do
- let(:runner) { create(:ci_runner, :project) }
+ describe '#heartbeat', :freeze_time do
+ let(:runner) { create(:ci_runner, :project, version: '15.0.0') }
let(:executor) { 'shell' }
- let(:version) { '15.0.1' }
+ let(:values) { { architecture: '18-bit', config: { gpus: "all" }, executor: executor, version: version } }
subject(:heartbeat) do
- runner.heartbeat(architecture: '18-bit', config: { gpus: "all" }, executor: executor, version: version)
+ runner.heartbeat(values)
end
context 'when database was updated recently' do
@@ -1090,29 +1086,61 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
runner.contacted_at = Time.current
end
- it 'updates cache' do
- expect_redis_update
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to receive(:perform_async)
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
+
+ before do
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
+ end
+
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner.runner_version).to be_nil
+ end
+
+ it 'schedules version information update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
+ end
- heartbeat
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'does not schedule version information update' do
+ heartbeat
- expect(runner.runner_version).to be_nil
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
end
context 'with only ip_address specified', :freeze_time do
- subject(:heartbeat) do
- runner.heartbeat(ip_address: '1.1.1.1')
+ let(:values) do
+ { ip_address: '1.1.1.1' }
end
it 'updates only ip_address' do
- attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
+ expect_redis_update(values.merge(contacted_at: Time.current))
- Gitlab::Redis::Cache.with do |redis|
- redis_key = runner.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, attrs, any_args)
+ heartbeat
+ end
+
+ context 'with new version having been cached' do
+ let(:version) { '15.0.1' }
+
+ before do
+ runner.cache_attributes(version: version)
end
- heartbeat
+ it 'does not lose cached version value' do
+ expect { heartbeat }.not_to change { runner.version }.from(version)
+ end
end
end
end
@@ -1121,65 +1149,81 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
before do
runner.contacted_at = 2.hours.ago
- allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async)
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version)
end
- context 'with invalid runner' do
- before do
- runner.runner_projects.delete_all
- end
+ context 'when version is changed' do
+ let(:version) { '15.0.1' }
- it 'still updates redis cache and database' do
- expect(runner).to be_invalid
+ context 'with invalid runner' do
+ before do
+ runner.runner_projects.delete_all
+ end
+ it 'still updates redis cache and database' do
+ expect(runner).to be_invalid
+
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
+ end
+ end
+
+ it 'updates redis cache and database' do
expect_redis_update
does_db_update
-
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).once
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).with(version).once
end
end
context 'with unchanged runner version' do
- let(:runner) { create(:ci_runner, version: version) }
+ let(:version) { runner.version }
it 'does not schedule ci_runner_versions update' do
heartbeat
expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
end
- end
- it 'updates redis cache and database' do
- expect_redis_update
- does_db_update
- expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).once
- end
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
- %w(custom shell docker docker-windows docker-ssh ssh parallels virtualbox docker+machine docker-ssh+machine kubernetes some-unknown-type).each do |executor|
- context "with #{executor} executor" do
- let(:executor) { executor }
+ it 'updates with expected executor type' do
+ expect_redis_update
- it 'updates with expected executor type' do
- expect_redis_update
+ heartbeat
- heartbeat
+ expect(runner.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
- expect(runner.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
end
+ end
+
+ context 'with an unknown executor type' do
+ let(:executor) { 'some-unknown-type' }
+
+ it 'updates with unknown executor type' do
+ expect_redis_update
- def expected_executor_type
- return 'unknown' if executor == 'some-unknown-type'
+ heartbeat
- executor.gsub(/[+-]/, '_')
+ expect(runner.reload.read_attribute(:executor_type)).to eq('unknown')
end
end
end
end
- def expect_redis_update
+ def expect_redis_update(values = anything)
+ values_json = values == anything ? anything : Gitlab::Json.dump(values)
+
Gitlab::Redis::Cache.with do |redis|
redis_key = runner.send(:cache_attribute_key)
- expect(redis).to receive(:set).with(redis_key, anything, any_args)
+ expect(redis).to receive(:set).with(redis_key, values_json, any_args).and_call_original
end
end
@@ -1994,4 +2038,59 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
end
+
+ describe '.with_creator' do
+ subject { described_class.with_creator }
+
+ let!(:user) { create(:admin) }
+ let!(:runner) { create(:ci_runner, creator: user) }
+
+ it { is_expected.to contain_exactly(runner) }
+ end
+
+ describe '#ensure_token' do
+ let(:runner) { described_class.new(registration_type: registration_type) }
+ let(:token) { 'an_existing_secret_token' }
+ let(:static_prefix) { described_class::CREATED_RUNNER_TOKEN_PREFIX }
+
+ context 'when runner is initialized without a token' do
+ context 'with registration_token' do
+ let(:registration_type) { :registration_token }
+
+ it 'generates a token' do
+ expect { runner.ensure_token }.to change { runner.token }.from(nil)
+ end
+ end
+
+ context 'with authenticated_user' do
+ let(:registration_type) { :authenticated_user }
+
+ it 'generates a token with prefix' do
+ expect { runner.ensure_token }.to change { runner.token }.from(nil).to(a_string_starting_with(static_prefix))
+ end
+ end
+ end
+
+ context 'when runner is initialized with a token' do
+ before do
+ runner.set_token(token)
+ end
+
+ context 'with registration_token' do
+ let(:registration_type) { :registration_token }
+
+ it 'does not change the existing token' do
+ expect { runner.ensure_token }.not_to change { runner.token }.from(token)
+ end
+ end
+
+ context 'with authenticated_user' do
+ let(:registration_type) { :authenticated_user }
+
+ it 'does not change the existing token' do
+ expect { runner.ensure_token }.not_to change { runner.token }.from(token)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb
index 51a2f14c57c..511d120ab7f 100644
--- a/spec/models/ci/runner_version_spec.rb
+++ b/spec/models/ci/runner_version_spec.rb
@@ -39,4 +39,15 @@ RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
describe 'validation' do
it { is_expected.to validate_length_of(:version).is_at_most(2048) }
end
+
+ describe '#status' do
+ context 'when is not processed' do
+ subject(:ci_runner_version) { create(:ci_runner_version, version: 'abc124', status: :not_processed) }
+
+ let(:attr) { :status }
+ let(:attr_value) { :not_processed }
+
+ it_behaves_like 'having enum with nil value'
+ end
+ end
end
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
index 707872d0a15..47f32353fef 100644
--- a/spec/models/ci/sources/pipeline_spec.rb
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -6,6 +6,11 @@ RSpec.describe Ci::Sources::Pipeline, feature_category: :continuous_integration
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:pipeline) }
+ it do
+ is_expected.to belong_to(:build).class_name('Ci::Build')
+ .with_foreign_key(:source_job_id).inverse_of(:sourced_pipelines)
+ end
+
it { is_expected.to belong_to(:source_project).class_name('::Project') }
it { is_expected.to belong_to(:source_job) }
it { is_expected.to belong_to(:source_bridge) }
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index ce64b3ea158..7a313115965 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Variable, feature_category: :pipeline_authoring do
+RSpec.describe Ci::Variable, feature_category: :pipeline_composition do
let_it_be_with_reload(:project) { create(:project) }
subject { build(:ci_variable, project: project) }
diff --git a/spec/models/clusters/applications/crossplane_spec.rb b/spec/models/clusters/applications/crossplane_spec.rb
deleted file mode 100644
index d1abaa52c7f..00000000000
--- a/spec/models/clusters/applications/crossplane_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Crossplane do
- let(:crossplane) { create(:clusters_applications_crossplane) }
-
- include_examples 'cluster application core specs', :clusters_applications_crossplane
- include_examples 'cluster application status specs', :clusters_applications_crossplane
- include_examples 'cluster application version specs', :clusters_applications_crossplane
- include_examples 'cluster application initial status specs'
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:stack) }
- end
-
- describe 'default values' do
- it { expect(subject.version).to eq(described_class::VERSION) }
- it { expect(subject.stack).to be_empty }
- end
-
- describe '#can_uninstall?' do
- subject { crossplane.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#install_command' do
- let(:stack) { 'gcp' }
-
- subject { crossplane.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with crossplane arguments' do
- expect(subject.name).to eq('crossplane')
- expect(subject.chart).to eq('crossplane/crossplane')
- expect(subject.repository).to eq('https://charts.crossplane.io/alpha')
- expect(subject.version).to eq('0.4.1')
- expect(subject).to be_rbac
- end
-
- context 'application failed to install previously' do
- let(:crossplane) { create(:clusters_applications_crossplane, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.4.1')
- end
- end
- end
-
- describe '#files' do
- let(:application) { crossplane }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes crossplane specific keys in the values.yaml file' do
- expect(values).to include('clusterStacks')
- end
- end
-end
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index f6b13f4a93f..91e90de02c0 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -17,10 +17,6 @@ RSpec.describe Clusters::Applications::Knative do
include_examples 'cluster application version specs', :clusters_applications_knative
include_examples 'cluster application initial status specs'
- describe 'associations' do
- it { is_expected.to have_one(:serverless_domain_cluster).class_name('::Serverless::DomainCluster').with_foreign_key('clusters_applications_knative_id').inverse_of(:knative) }
- end
-
describe 'default values' do
it { expect(subject.version).to eq(described_class::VERSION) }
end
@@ -135,19 +131,6 @@ RSpec.describe Clusters::Applications::Knative do
it 'does not install metrics for prometheus' do
expect(subject.postinstall).to be_empty
end
-
- context 'with prometheus installed' do
- let(:prometheus) { create(:clusters_applications_prometheus, :installed) }
- let(:knative) { create(:clusters_applications_knative, cluster: prometheus.cluster) }
-
- subject { knative.install_command }
-
- it 'installs metrics' do
- expect(subject.postinstall).not_to be_empty
- expect(subject.postinstall.length).to be(1)
- expect(subject.postinstall[0]).to eql("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
- end
- end
end
describe '#install_command' do
@@ -249,12 +232,4 @@ RSpec.describe Clusters::Applications::Knative do
expect(subject.find_available_domain(domain.id)).to eq(domain)
end
end
-
- describe '#pages_domain' do
- let!(:sdc) { create(:serverless_domain_cluster, knative: knative) }
-
- it 'returns the the associated pages domain' do
- expect(knative.reload.pages_domain).to eq(sdc.pages_domain)
- end
- end
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
deleted file mode 100644
index 15c3162270e..00000000000
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ /dev/null
@@ -1,349 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Prometheus do
- include KubernetesHelpers
- include StubRequests
-
- include_examples 'cluster application core specs', :clusters_applications_prometheus
- include_examples 'cluster application status specs', :clusters_applications_prometheus
- include_examples 'cluster application version specs', :clusters_applications_prometheus
- include_examples 'cluster application helm specs', :clusters_applications_prometheus
- include_examples 'cluster application initial status specs'
-
- describe 'default values' do
- subject(:prometheus) { build(:clusters_applications_prometheus) }
-
- it { expect(prometheus.alert_manager_token).to be_an_instance_of(String) }
- it { expect(prometheus.version).to eq(described_class::VERSION) }
- end
-
- describe 'after_destroy' do
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
-
- it 'disables the corresponding integration' do
- application.destroy!
-
- expect(cluster.integration_prometheus).not_to be_enabled
- end
- end
-
- describe 'transition to installed' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
-
- it 'enables the corresponding integration' do
- application.make_installed
-
- expect(cluster.integration_prometheus).to be_enabled
- end
- end
-
- describe 'transition to externally_installed' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
-
- it 'enables the corresponding integration' do
- application.make_externally_installed!
-
- expect(cluster.integration_prometheus).to be_enabled
- end
- end
-
- describe 'transition to updating' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, projects: [project]) }
-
- subject { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
-
- it 'sets last_update_started_at to now' do
- freeze_time do
- expect { subject.make_updating }.to change { subject.reload.last_update_started_at }.to be_within(1.second).of(Time.current)
- end
- end
- end
-
- describe '#managed_prometheus?' do
- subject { prometheus.managed_prometheus? }
-
- let(:prometheus) { build(:clusters_applications_prometheus) }
-
- it { is_expected.to be_truthy }
-
- context 'externally installed' do
- let(:prometheus) { build(:clusters_applications_prometheus, :externally_installed) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'uninstalled' do
- let(:prometheus) { build(:clusters_applications_prometheus, :uninstalled) }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#can_uninstall?' do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- subject { prometheus.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#prometheus_client' do
- include_examples '#prometheus_client shared' do
- let(:factory) { :clusters_applications_prometheus }
- end
- end
-
- describe '#install_command' do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- subject { prometheus.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with 3 arguments' do
- expect(subject.name).to eq('prometheus')
- expect(subject.chart).to eq('prometheus/prometheus')
- expect(subject.version).to eq('10.4.1')
- expect(subject).to be_rbac
- expect(subject.files).to eq(prometheus.files)
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- prometheus.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' do
- let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('10.4.1')
- end
- end
-
- it 'does not install knative metrics' do
- expect(subject.postinstall).to be_empty
- end
-
- context 'with knative installed' do
- let(:knative) { create(:clusters_applications_knative, :updated) }
- let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
-
- subject { prometheus.install_command }
-
- it 'installs knative metrics' do
- expect(subject.postinstall).to include("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
- end
- end
- end
-
- describe '#uninstall_command' do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- subject { prometheus.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) }
-
- it 'has the application name' do
- expect(subject.name).to eq('prometheus')
- end
-
- it 'has files' do
- expect(subject.files).to eq(prometheus.files)
- end
-
- it 'is rbac' do
- expect(subject).to be_rbac
- end
-
- describe '#predelete' do
- let(:knative) { create(:clusters_applications_knative, :updated) }
- let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
-
- subject { prometheus.uninstall_command.predelete }
-
- it 'deletes knative metrics' do
- metrics_config = Clusters::Applications::Knative::METRICS_CONFIG
- is_expected.to include("kubectl delete -f #{metrics_config} --ignore-not-found")
- end
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- prometheus.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
- end
-
- describe '#patch_command' do
- subject(:patch_command) { prometheus.patch_command(values) }
-
- let(:prometheus) { build(:clusters_applications_prometheus) }
- let(:values) { prometheus.values }
-
- it { is_expected.to be_an_instance_of(::Gitlab::Kubernetes::Helm::V3::PatchCommand) }
-
- it 'is initialized with 3 arguments' do
- expect(patch_command.name).to eq('prometheus')
- expect(patch_command.chart).to eq('prometheus/prometheus')
- expect(patch_command.version).to eq('10.4.1')
- expect(patch_command.files).to eq(prometheus.files)
- end
- end
-
- describe '#update_in_progress?' do
- context 'when app is updating' do
- it 'returns true' do
- cluster = create(:cluster)
- prometheus_app = build(:clusters_applications_prometheus, :updating, cluster: cluster)
-
- expect(prometheus_app.update_in_progress?).to be true
- end
- end
- end
-
- describe '#update_errored?' do
- context 'when app errored' do
- it 'returns true' do
- cluster = create(:cluster)
- prometheus_app = build(:clusters_applications_prometheus, :update_errored, cluster: cluster)
-
- expect(prometheus_app.update_errored?).to be true
- end
- end
- end
-
- describe '#files' do
- let(:application) { create(:clusters_applications_prometheus) }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes prometheus valid values' do
- expect(values).to include('alertmanager')
- expect(values).to include('kubeStateMetrics')
- expect(values).to include('nodeExporter')
- expect(values).to include('pushgateway')
- expect(values).to include('serverFiles')
- end
- end
-
- describe '#files_with_replaced_values' do
- let(:application) { build(:clusters_applications_prometheus) }
- let(:files) { application.files }
-
- subject { application.files_with_replaced_values({ hello: :world }) }
-
- it 'does not modify #files' do
- expect(subject[:'values.yaml']).not_to eq(files[:'values.yaml'])
-
- expect(files[:'values.yaml']).to eq(application.values)
- end
-
- it 'returns values.yaml with replaced values' do
- expect(subject[:'values.yaml']).to eq({ hello: :world })
- end
-
- it 'uses values from #files, except for values.yaml' do
- allow(application).to receive(:files).and_return({
- 'values.yaml': 'some value specific to files',
- 'file_a.txt': 'file_a',
- 'file_b.txt': 'file_b'
- })
-
- expect(subject.except(:'values.yaml')).to eq({
- 'file_a.txt': 'file_a',
- 'file_b.txt': 'file_b'
- })
- end
- end
-
- describe '#configured?' do
- let(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
-
- subject { prometheus.configured? }
-
- context 'when a kubenetes client is present' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-
- it { is_expected.to be_truthy }
-
- context 'when it is not availalble' do
- let(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when the kubernetes URL is blocked' do
- before do
- blocked_ip = '127.0.0.1' # localhost addresses are blocked by default
-
- stub_all_dns(cluster.platform.api_url, ip_address: blocked_ip)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when a kubenetes client is not present' do
- let(:cluster) { create(:cluster) }
-
- it { is_expected.to be_falsy }
- end
- end
-
- describe '#updated_since?' do
- let(:cluster) { create(:cluster) }
- let(:prometheus_app) { build(:clusters_applications_prometheus, cluster: cluster) }
- let(:timestamp) { Time.current - 5.minutes }
-
- around do |example|
- freeze_time { example.run }
- end
-
- before do
- prometheus_app.last_update_started_at = Time.current
- end
-
- context 'when app does not have status failed' do
- it 'returns true when last update started after the timestamp' do
- expect(prometheus_app.updated_since?(timestamp)).to be true
- end
-
- it 'returns false when last update started before the timestamp' do
- expect(prometheus_app.updated_since?(Time.current + 5.minutes)).to be false
- end
- end
-
- context 'when app has status failed' do
- it 'returns false when last update started after the timestamp' do
- prometheus_app.status = 6
-
- expect(prometheus_app.updated_since?(timestamp)).to be false
- end
- end
- end
-
- describe 'alert manager token' do
- subject { create(:clusters_applications_prometheus) }
-
- it 'is autogenerated on creation' do
- expect(subject.alert_manager_token).to match(/\A\h{32}\z/)
- expect(subject.encrypted_alert_manager_token).not_to be_nil
- expect(subject.encrypted_alert_manager_token_iv).not_to be_nil
- end
- end
-end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 2a2e2899d24..2d46714eb22 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -25,7 +25,6 @@ feature_category: :kubernetes_management do
it { is_expected.to have_one(:integration_prometheus) }
it { is_expected.to have_one(:application_helm) }
it { is_expected.to have_one(:application_ingress) }
- it { is_expected.to have_one(:application_prometheus) }
it { is_expected.to have_one(:application_runner) }
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:cluster_project) }
@@ -714,13 +713,12 @@ feature_category: :kubernetes_management do
context 'when all applications are created' do
let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
- let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
+ is_expected.to contain_exactly(helm, ingress, runner, jupyter, knative)
end
end
@@ -1342,22 +1340,6 @@ feature_category: :kubernetes_management do
expect(cluster.prometheus_adapter).to eq(integration)
end
end
-
- context 'has application_prometheus' do
- let_it_be(:application) { create(:clusters_applications_prometheus, :no_helm_installed, cluster: cluster) }
-
- it 'returns nil' do
- expect(cluster.prometheus_adapter).to be_nil
- end
-
- context 'also has a integration_prometheus' do
- let_it_be(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it 'returns the integration' do
- expect(cluster.prometheus_adapter).to eq(integration)
- end
- end
- end
end
describe '#delete_cached_resources!' do
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index b280275c2e5..efe511bf1c5 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -930,4 +930,13 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
end
end
+
+ describe '#authorization_type' do
+ subject(:kubernetes) { create(:cluster_platform_kubernetes) }
+
+ let(:attr) { :authorization_type }
+ let(:attr_value) { :unknown_authorization }
+
+ it_behaves_like 'having enum with nil value'
+ end
end
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index 6dd34c3e21f..706f18a5337 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CommitCollection do
+RSpec.describe CommitCollection, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit("c1c67abbaf91f624347bb3ae96eabe3a1b742478") }
@@ -191,6 +191,19 @@ RSpec.describe CommitCollection do
end
end
+ describe '#load_tags' do
+ let(:gitaly_commit_with_tags) { project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let(:collection) { described_class.new(project, [gitaly_commit_with_tags]) }
+
+ subject { collection.load_tags }
+
+ it 'loads tags' do
+ subject
+
+ expect(collection.commits[0].referenced_by).to contain_exactly('refs/tags/v1.1.0')
+ end
+ end
+
describe '#respond_to_missing?' do
it 'returns true when the underlying Array responds to the message' do
collection = described_class.new(project, [])
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 4ff451af9de..54ca7f0c893 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -17,7 +17,11 @@ RSpec.describe CommitStatus do
it_behaves_like 'having unique enum values'
- it { is_expected.to belong_to(:pipeline) }
+ it do
+ is_expected.to belong_to(:pipeline).class_name('Ci::Pipeline')
+ .with_foreign_key(:commit_id).inverse_of(:statuses)
+ end
+
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:auto_canceled_by) }
@@ -33,6 +37,7 @@ RSpec.describe CommitStatus do
it { is_expected.to respond_to :running? }
it { is_expected.to respond_to :pending? }
it { is_expected.not_to be_retried }
+ it { expect(described_class.primary_key).to eq('id') }
describe '#author' do
subject { commit_status.author }
@@ -422,29 +427,6 @@ RSpec.describe CommitStatus do
end
end
- describe '.exclude_ignored' do
- subject { described_class.exclude_ignored.order(:id) }
-
- let(:statuses) do
- [create_status(when: 'manual', status: 'skipped'),
- create_status(when: 'manual', status: 'success'),
- create_status(when: 'manual', status: 'failed'),
- create_status(when: 'on_failure', status: 'skipped'),
- create_status(when: 'on_failure', status: 'success'),
- create_status(when: 'on_failure', status: 'failed'),
- create_status(allow_failure: true, status: 'success'),
- create_status(allow_failure: true, status: 'failed'),
- create_status(allow_failure: false, status: 'success'),
- create_status(allow_failure: false, status: 'failed'),
- create_status(allow_failure: true, status: 'manual'),
- create_status(allow_failure: false, status: 'manual')]
- end
-
- it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
- end
- end
-
describe '.failed_but_allowed' do
subject { described_class.failed_but_allowed.order(:id) }
@@ -1062,4 +1044,13 @@ RSpec.describe CommitStatus do
end
end
end
+
+ describe '#failure_reason' do
+ subject(:status) { commit_status }
+
+ let(:attr) { :failure_reason }
+ let(:attr_value) { :unknown_failure }
+
+ it_behaves_like 'having enum with nil value'
+ end
end
diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb
index 5fe3141eb17..625d8fec0fb 100644
--- a/spec/models/concerns/atomic_internal_id_spec.rb
+++ b/spec/models/concerns/atomic_internal_id_spec.rb
@@ -250,11 +250,104 @@ RSpec.describe AtomicInternalId do
end
end
- describe '.track_project_iid!' do
+ describe '.track_namespace_iid!' do
it 'tracks the present value' do
expect do
- ::Issue.track_project_iid!(milestone.project, external_iid)
- end.to change { InternalId.find_by(project: milestone.project, usage: :issues)&.last_value.to_i }.to(external_iid)
+ ::Issue.track_namespace_iid!(milestone.project.project_namespace, external_iid)
+ end.to change {
+ InternalId.find_by(namespace: milestone.project.project_namespace, usage: :issues)&.last_value.to_i
+ }.to(external_iid)
+ end
+ end
+
+ context 'when transitioning a model from one scope to another' do
+ let!(:issue) { build(:issue, project: project) }
+ let(:old_issue_model) do
+ Class.new(ApplicationRecord) do
+ include AtomicInternalId
+
+ self.table_name = :issues
+
+ belongs_to :project
+ belongs_to :namespace
+
+ has_internal_id :iid, scope: :project
+
+ def self.name
+ 'TestClassA'
+ end
+ end
+ end
+
+ let(:old_issue_instance) { old_issue_model.new(issue.attributes) }
+ let(:new_issue_instance) { Issue.new(issue.attributes) }
+
+ it 'generates the iid on the new scope' do
+ # set a random iid, just so that it does not start at 1
+ old_issue_instance.iid = 123
+ old_issue_instance.save!
+
+ # creating a new old_issue_instance increments the iid.
+ expect { old_issue_model.new(issue.attributes).save! }.to change {
+ InternalId.find_by(project: project, usage: :issues)&.last_value.to_i
+ }.from(123).to(124).and(not_change { InternalId.count })
+
+ # creating a new Issue creates a new record in internal_ids, scoped to the namespace.
+ # Given the Issue#has_internal_id -> init definition the internal_ids#last_value would be the
+ # maximum between the old iid value in internal_ids, scoped to the project and max(iid) value from issues
+ # table by namespace_id.
+ # see Issue#has_internal_id
+ expect { new_issue_instance.save! }.to change {
+ InternalId.find_by(namespace: project.project_namespace, usage: :issues)&.last_value.to_i
+ }.to(125).and(change { InternalId.count }.by(1))
+
+ # transition back to project scope would generate overlapping IIDs and raise a duplicate key value error, unless
+ # we cleanup the issues usage scoped to the project first
+ expect { old_issue_model.new(issue.attributes).save! }.to raise_error(ActiveRecord::RecordNotUnique)
+
+ # delete issues usage scoped to te project
+ InternalId.where(project: project, usage: :issues).delete_all
+
+ expect { old_issue_model.new(issue.attributes).save! }.to change {
+ InternalId.find_by(project: project, usage: :issues)&.last_value.to_i
+ }.to(126).and(change { InternalId.count }.by(1))
+ end
+ end
+
+ context 'when models is scoped to namespace and does not have an init proc' do
+ let!(:issue) { build(:issue, namespace: create(:group)) }
+
+ let(:issue_model) do
+ Class.new(ApplicationRecord) do
+ include AtomicInternalId
+
+ self.table_name = :issues
+
+ belongs_to :project
+ belongs_to :namespace
+
+ has_internal_id :iid, scope: :namespace
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+
+ let(:model_instance) { issue_model.new(issue.attributes) }
+
+ it 'generates the iid on the new scope' do
+ expect { model_instance.save! }.to change {
+ InternalId.find_by(namespace: model_instance.namespace, usage: :issues)&.last_value.to_i
+ }.to(1).and(change { InternalId.count }.by(1))
+ end
+
+ it 'supplies a stream of iid values' do
+ expect do
+ issue_model.with_namespace_iid_supply(model_instance.namespace) do |supply|
+ 4.times { supply.next_value }
+ end
+ end.to change { InternalId.find_by(namespace: model_instance.namespace, usage: :issues)&.last_value.to_i }.by(4)
end
end
end
diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb
index b57b2b15608..12157867062 100644
--- a/spec/models/concerns/ci/maskable_spec.rb
+++ b/spec/models/concerns/ci/maskable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Maskable, feature_category: :pipeline_authoring do
+RSpec.describe Ci::Maskable, feature_category: :pipeline_composition do
let(:variable) { build(:ci_variable) }
describe 'masked value validations' do
diff --git a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb b/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb
deleted file mode 100644
index bb25d7d1665..00000000000
--- a/spec/models/concerns/ci/partitionable/partitioned_filter_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::Partitionable::PartitionedFilter, :aggregate_failures, feature_category: :continuous_integration do
- before do
- create_tables(<<~SQL)
- CREATE TABLE _test_ci_jobs_metadata (
- id serial NOT NULL,
- partition_id int NOT NULL DEFAULT 10,
- name text,
- PRIMARY KEY (id, partition_id)
- ) PARTITION BY LIST(partition_id);
-
- CREATE TABLE _test_ci_jobs_metadata_1
- PARTITION OF _test_ci_jobs_metadata
- FOR VALUES IN (10);
- SQL
- end
-
- let(:model) do
- Class.new(Ci::ApplicationRecord) do
- include Ci::Partitionable::PartitionedFilter
-
- self.primary_key = :id
- self.table_name = :_test_ci_jobs_metadata
-
- def self.name
- 'TestCiJobMetadata'
- end
- end
- end
-
- let!(:record) { model.create! }
-
- let(:where_filter) do
- /WHERE "_test_ci_jobs_metadata"."id" = #{record.id} AND "_test_ci_jobs_metadata"."partition_id" = 10/
- end
-
- describe '#save' do
- it 'uses id and partition_id' do
- record.name = 'test'
- recorder = ActiveRecord::QueryRecorder.new { record.save! }
-
- expect(recorder.log).to include(where_filter)
- expect(record.name).to eq('test')
- end
- end
-
- describe '#update' do
- it 'uses id and partition_id' do
- recorder = ActiveRecord::QueryRecorder.new { record.update!(name: 'test') }
-
- expect(recorder.log).to include(where_filter)
- expect(record.name).to eq('test')
- end
- end
-
- describe '#delete' do
- it 'uses id and partition_id' do
- recorder = ActiveRecord::QueryRecorder.new { record.delete }
-
- expect(recorder.log).to include(where_filter)
- expect(model.count).to be_zero
- end
- end
-
- describe '#destroy' do
- it 'uses id and partition_id' do
- recorder = ActiveRecord::QueryRecorder.new { record.destroy! }
-
- expect(recorder.log).to include(where_filter)
- expect(model.count).to be_zero
- end
- end
-
- def create_tables(table_sql)
- Ci::ApplicationRecord.connection.execute(table_sql)
- end
-end
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index 430ef57d493..5100f20ed25 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -52,16 +52,16 @@ RSpec.describe Ci::Partitionable do
context 'when partitioned is true' do
let(:partitioned) { true }
- it { expect(ci_model.ancestors).to include(described_class::PartitionedFilter) }
- it { expect(ci_model).to be_partitioned }
+ it { expect(ci_model.ancestors).to include(PartitionedTable) }
+ it { expect(ci_model.partitioning_strategy).to be_a(Gitlab::Database::Partitioning::CiSlidingListStrategy) }
+ it { expect(ci_model.partitioning_strategy.partitioning_key).to eq(:partition_id) }
end
context 'when partitioned is false' do
let(:partitioned) { false }
- it { expect(ci_model.ancestors).not_to include(described_class::PartitionedFilter) }
-
- it { expect(ci_model).not_to be_partitioned }
+ it { expect(ci_model.ancestors).not_to include(PartitionedTable) }
+ it { expect(ci_model).not_to respond_to(:partitioning_strategy) }
end
end
end
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
index 2c75d4d5c41..75c5cac899b 100644
--- a/spec/models/concerns/each_batch_spec.rb
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -171,4 +171,36 @@ RSpec.describe EachBatch do
end
end
end
+
+ describe '.each_batch_count' do
+ let_it_be(:users) { create_list(:user, 5, updated_at: 1.day.ago) }
+
+ it 'counts the records' do
+ count, last_value = User.each_batch_count
+
+ expect(count).to eq(5)
+ expect(last_value).to eq(nil)
+ end
+
+ context 'when using a different column' do
+ it 'returns correct count' do
+ count, _ = User.each_batch_count(column: :email, of: 2)
+
+ expect(count).to eq(5)
+ end
+ end
+
+ context 'when stopping and resuming the counting' do
+ it 'returns the correct count' do
+ count, last_value = User.each_batch_count(of: 1) do |current_count, _current_value|
+ current_count == 3 # stop when count reaches 3
+ end
+
+ expect(count).to eq(3)
+
+ final_count, _ = User.each_batch_count(of: 1, last_value: last_value, last_count: count)
+ expect(final_count).to eq(5)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index bd128112113..03d2c267098 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe User do
+RSpec.describe User, feature_category: :system_access do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES.keys)
.to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot
- migration_bot automation_bot admin_bot suggested_reviewers_bot])
+ migration_bot automation_bot security_policy_bot admin_bot suggested_reviewers_bot service_account])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index c270f23defb..bf277b094ea 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -50,6 +50,58 @@ RSpec.describe RedisCacheable do
subject
end
+
+ context 'with existing cached attributes' do
+ before do
+ instance.cache_attributes({ existing_attr: 'value' })
+ end
+
+ it 'sets the cache attributes' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:set).with(cache_key, payload.to_json, anything).and_call_original
+ end
+
+ expect { subject }.to change { instance.cached_attribute(:existing_attr) }.from('value').to(nil)
+ end
+ end
+ end
+
+ describe '#merge_cache_attributes' do
+ subject { instance.merge_cache_attributes(payload) }
+
+ let(:existing_attributes) { { existing_attr: 'value', name: 'value' } }
+
+ before do
+ instance.cache_attributes(existing_attributes)
+ end
+
+ context 'with different attribute values' do
+ let(:payload) { { name: 'new_value' } }
+
+ it 'merges the cache attributes with existing values' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).to receive(:set).with(cache_key, existing_attributes.merge(payload).to_json, anything)
+ .and_call_original
+ end
+
+ subject
+
+ expect(instance.cached_attribute(:existing_attr)).to eq 'value'
+ expect(instance.cached_attribute(:name)).to eq 'new_value'
+ end
+ end
+
+ context 'with no new or changed attribute values' do
+ let(:payload) { { name: 'value' } }
+
+ it 'does not try to set Redis key' do
+ Gitlab::Redis::Cache.with do |redis|
+ expect(redis).not_to receive(:set)
+ end
+
+ subject
+ end
+ end
end
describe '#cached_attr_reader', :clean_gitlab_redis_cache do
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index dc1002f3560..0bbe3dea812 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -74,6 +74,17 @@ RSpec.shared_examples 'routable resource with parent' do
describe '#full_name' do
it { expect(record.full_name).to eq "#{record.parent.human_name} / #{record.name}" }
+ context 'without route name' do
+ before do
+ stub_feature_flags(cached_route_lookups: true)
+ record.route.update_attribute(:name, nil)
+ end
+
+ it 'builds full name' do
+ expect(record.full_name).to eq("#{record.parent.human_name} / #{record.name}")
+ end
+ end
+
it 'hits the cache when not preloaded' do
forcibly_hit_cached_lookup(record, :full_name)
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index a60a0a5e26d..80060802de8 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -215,5 +215,31 @@ RSpec.describe Subscribable, 'Subscribable' do
expect(lazy_queries.count).to eq(0)
expect(preloaded_queries.count).to eq(1)
end
+
+ context 'with work items' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ before do
+ [issue, work_item].each do |item|
+ create(:subscription, user: user, subscribable: item, subscribed: true, project: project)
+ end
+ end
+
+ it 'loads correct subscribable type' do
+ expect(issue).to receive(:subscribable_type).and_return('Issue')
+ issue.lazy_subscription(user, project)
+
+ expect(work_item).to receive(:subscribable_type).and_return('Issue')
+ work_item.lazy_subscription(user, project)
+ end
+
+ it 'matches existing subscription type' do
+ expect(issue.subscribed?(user, project)).to eq(true)
+ expect(work_item.subscribed?(user, project)).to eq(true)
+ end
+ end
end
end
diff --git a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
index 89ddc797a9d..2679bd7d93b 100644
--- a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TokenAuthenticatableStrategies::Base do
+RSpec.describe TokenAuthenticatableStrategies::Base, feature_category: :system_access do
let(:instance) { double(:instance) }
let(:field) { double(:field) }
@@ -24,6 +24,41 @@ RSpec.describe TokenAuthenticatableStrategies::Base do
end
end
+ describe '#format_token' do
+ let(:strategy) { described_class.new(instance, field, options) }
+
+ let(:instance) { build(:ci_build, name: 'build_name_for_format_option', partition_id: partition_id) }
+ let(:partition_id) { 100 }
+ let(:field) { 'token' }
+ let(:options) { {} }
+
+ let(:token) { 'a_secret_token' }
+
+ it 'returns the origin token' do
+ expect(strategy.format_token(instance, token)).to eq(token)
+ end
+
+ context 'when format_with_prefix option is provided' do
+ context 'with symbol' do
+ let(:options) { { format_with_prefix: :partition_id_prefix_in_16_bit_encode } }
+ let(:partition_id_in_16_bit_encode_with_underscore) { "#{partition_id.to_s(16)}_" }
+ let(:formatted_token) { "#{partition_id_in_16_bit_encode_with_underscore}#{token}" }
+
+ it 'returns a formatted token from the format_with_prefix option' do
+ expect(strategy.format_token(instance, token)).to eq(formatted_token)
+ end
+ end
+
+ context 'with something else' do
+ let(:options) { { format_with_prefix: false } }
+
+ it 'raise not implemented' do
+ expect { strategy.format_token(instance, token) }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+ end
+
describe '.fabricate' do
context 'when digest stragegy is specified' do
it 'fabricates digest strategy object' do
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index 2df86804f34..3bdb0647d62 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -2,16 +2,20 @@
require 'spec_helper'
-RSpec.describe TokenAuthenticatableStrategies::Encrypted do
+RSpec.describe TokenAuthenticatableStrategies::Encrypted, feature_category: :system_access do
let(:model) { double(:model) }
let(:instance) { double(:instance) }
+ let(:original_token) { 'my-value' }
+ let(:resource) { double(:resource) }
+ let(:options) { other_options.merge(encrypted: encrypted_option) }
+ let(:other_options) { {} }
let(:encrypted) do
- TokenAuthenticatableStrategies::EncryptionHelper.encrypt_token('my-value')
+ TokenAuthenticatableStrategies::EncryptionHelper.encrypt_token(original_token)
end
let(:encrypted_with_static_iv) do
- Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value')
+ Gitlab::CryptoHelper.aes256_gcm_encrypt(original_token)
end
subject(:strategy) do
@@ -19,7 +23,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
describe '#token_fields' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
it 'includes the encrypted field' do
expect(strategy.token_fields).to contain_exactly('some_field', 'some_field_encrypted')
@@ -27,50 +31,70 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
describe '#find_token_authenticatable' do
+ shared_examples 'finds the resource' do
+ it 'finds the resource by cleartext' do
+ expect(subject.find_token_authenticatable(original_token))
+ .to eq(resource)
+ end
+ end
+
+ shared_examples 'does not find any resource' do
+ it 'does not find any resource by cleartext' do
+ expect(subject.find_token_authenticatable(original_token))
+ .to be_nil
+ end
+ end
+
+ shared_examples 'finds the resource with/without setting require_prefix_for_validation' do
+ let(:standard_runner_token_prefix) { 'GR1348941' }
+ it_behaves_like 'finds the resource'
+
+ context 'when a require_prefix_for_validation is provided' do
+ let(:other_options) { { format_with_prefix: :format_with_prefix_method, require_prefix_for_validation: true } }
+
+ before do
+ allow(resource).to receive(:format_with_prefix_method).and_return(standard_runner_token_prefix)
+ end
+
+ it_behaves_like 'does not find any resource'
+
+ context 'when token starts with prefix' do
+ let(:original_token) { "#{standard_runner_token_prefix}plain_token" }
+
+ it_behaves_like 'finds the resource'
+ end
+ end
+ end
+
context 'when encryption is required' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
+ let(:resource) { double(:encrypted_resource) }
- it 'finds the encrypted resource by cleartext' do
+ before do
allow(model).to receive(:where)
.and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to eq 'encrypted resource'
+ .and_return(resource)
end
- context 'when a prefix is required' do
- let(:options) { { encrypted: :required, prefix: 'GR1348941' } }
-
- it 'finds the encrypted resource by cleartext' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
- end
+ it_behaves_like 'finds the resource with/without setting require_prefix_for_validation'
end
context 'when encryption is optional' do
- let(:options) { { encrypted: :optional } }
+ let(:encrypted_option) { :optional }
+ let(:resource) { double(:encrypted_resource) }
- it 'finds the encrypted resource by cleartext' do
+ before do
allow(model).to receive(:where)
.and_return(model)
allow(model).to receive(:find_by)
.with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to eq 'encrypted resource'
+ .and_return(resource)
end
+ it_behaves_like 'finds the resource with/without setting require_prefix_for_validation'
+
it 'uses insecure strategy when encrypted token cannot be found' do
allow(subject.send(:insecure_strategy))
.to receive(:find_token_authenticatable)
@@ -85,68 +109,27 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
expect(subject.find_token_authenticatable('my-value'))
.to eq 'plaintext resource'
end
-
- context 'when a prefix is required' do
- let(:options) { { encrypted: :optional, prefix: 'GR1348941' } }
-
- it 'finds the encrypted resource by cleartext' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field_encrypted' => [encrypted, encrypted_with_static_iv])
- .and_return('encrypted resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
- end
end
context 'when encryption is migrating' do
- let(:options) { { encrypted: :migrating } }
+ let(:encrypted_option) { :migrating }
+ let(:resource) { double(:cleartext_resource) }
- it 'finds the cleartext resource by cleartext' do
+ before do
allow(model).to receive(:where)
.and_return(model)
allow(model).to receive(:find_by)
- .with('some_field' => 'my-value')
- .and_return('cleartext resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to eq 'cleartext resource'
+ .with('some_field' => original_token)
+ .and_return(resource)
end
- it 'returns nil if resource cannot be found' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field' => 'my-value')
- .and_return(nil)
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
-
- context 'when a prefix is required' do
- let(:options) { { encrypted: :migrating, prefix: 'GR1348941' } }
-
- it 'finds the encrypted resource by cleartext' do
- allow(model).to receive(:where)
- .and_return(model)
- allow(model).to receive(:find_by)
- .with('some_field' => 'my-value')
- .and_return('cleartext resource')
-
- expect(subject.find_token_authenticatable('my-value'))
- .to be_nil
- end
- end
+ it_behaves_like 'finds the resource with/without setting require_prefix_for_validation'
end
end
describe '#get_token' do
context 'when encryption is required' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
it 'returns decrypted token when an encrypted with static iv token is present' do
allow(instance).to receive(:read_attribute)
@@ -166,7 +149,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is optional' do
- let(:options) { { encrypted: :optional } }
+ let(:encrypted_option) { :optional }
it 'returns decrypted token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
@@ -198,7 +181,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is migrating' do
- let(:options) { { encrypted: :migrating } }
+ let(:encrypted_option) { :migrating }
it 'returns cleartext token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
@@ -228,7 +211,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
describe '#set_token' do
context 'when encryption is required' do
- let(:options) { { encrypted: :required } }
+ let(:encrypted_option) { :required }
it 'writes encrypted token and returns it' do
expect(instance).to receive(:[]=)
@@ -239,7 +222,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is optional' do
- let(:options) { { encrypted: :optional } }
+ let(:encrypted_option) { :optional }
it 'writes encrypted token and removes plaintext token and returns it' do
expect(instance).to receive(:[]=)
@@ -252,7 +235,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when encryption is migrating' do
- let(:options) { { encrypted: :migrating } }
+ let(:encrypted_option) { :migrating }
it 'writes encrypted token and writes plaintext token' do
expect(instance).to receive(:[]=)
diff --git a/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
index 671e51e3913..004298bbdd9 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
@@ -6,96 +6,26 @@ RSpec.describe TokenAuthenticatableStrategies::EncryptionHelper do
let(:encrypted_token) { described_class.encrypt_token('my-value-my-value-my-value') }
describe '.encrypt_token' do
- context 'when dynamic_nonce feature flag is switched on' do
- it 'adds nonce identifier on the beginning' do
- expect(encrypted_token.first).to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
- end
-
- it 'adds nonce at the end' do
- nonce = encrypted_token.last(described_class::NONCE_SIZE)
-
- expect(nonce).to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
- end
-
- it 'encrypts token' do
- expect(encrypted_token[1...-described_class::NONCE_SIZE]).not_to eq('my-value-my-value-my-value')
- end
+ it 'adds nonce identifier on the beginning' do
+ expect(encrypted_token.first).to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
end
- context 'when dynamic_nonce feature flag is switched off' do
- before do
- stub_feature_flags(dynamic_nonce: false)
- end
-
- it 'does not add nonce identifier on the beginning' do
- expect(encrypted_token.first).not_to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
- end
-
- it 'does not add nonce in the end' do
- nonce = encrypted_token.last(described_class::NONCE_SIZE)
-
- expect(nonce).not_to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
- end
+ it 'adds nonce at the end' do
+ nonce = encrypted_token.last(described_class::NONCE_SIZE)
- it 'encrypts token with static iv' do
- token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value-my-value-my-value')
+ expect(nonce).to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
+ end
- expect(encrypted_token).to eq(token)
- end
+ it 'encrypts token' do
+ expect(encrypted_token[1...-described_class::NONCE_SIZE]).not_to eq('my-value-my-value-my-value')
end
end
describe '.decrypt_token' do
- context 'with feature flag switched off' do
- before do
- stub_feature_flags(dynamic_nonce: false)
- end
-
- it 'decrypts token with static iv' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
-
- it 'decrypts token if feature flag changed after encryption' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(encrypted_token).not_to eq('my-value')
-
- stub_feature_flags(dynamic_nonce: true)
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
-
- it 'decrypts token with dynamic iv' do
- iv = ::Digest::SHA256.hexdigest('my-value').bytes.take(described_class::NONCE_SIZE).pack('c*')
- token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value', nonce: iv)
- encrypted_token = "#{described_class::DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv}"
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
- end
-
- context 'with feature flag switched on' do
- before do
- stub_feature_flags(dynamic_nonce: true)
- end
-
- it 'decrypts token with dynamic iv' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
-
- it 'decrypts token if feature flag changed after encryption' do
- encrypted_token = described_class.encrypt_token('my-value')
-
- expect(encrypted_token).not_to eq('my-value')
-
- stub_feature_flags(dynamic_nonce: false)
+ it 'decrypts token with dynamic iv' do
+ encrypted_token = described_class.encrypt_token('my-value')
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
- end
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
end
end
end
diff --git a/spec/models/concerns/web_hooks/has_web_hooks_spec.rb b/spec/models/concerns/web_hooks/has_web_hooks_spec.rb
new file mode 100644
index 00000000000..afb2406a969
--- /dev/null
+++ b/spec/models/concerns/web_hooks/has_web_hooks_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebHooks::HasWebHooks, feature_category: :integrations do
+ let(:minimal_test_class) do
+ Class.new do
+ include WebHooks::HasWebHooks
+
+ def id
+ 1
+ end
+ end
+ end
+
+ before do
+ stub_const('MinimalTestClass', minimal_test_class)
+ end
+
+ describe '#last_failure_redis_key' do
+ subject { MinimalTestClass.new.last_failure_redis_key }
+
+ it { is_expected.to eq('web_hooks:last_failure:minimal_test_class-1') }
+ end
+
+ describe 'last_webhook_failure', :clean_gitlab_redis_shared_state do
+ subject { MinimalTestClass.new.last_webhook_failure }
+
+ it { is_expected.to eq(nil) }
+
+ context 'when there was an older failure', :clean_gitlab_redis_shared_state do
+ let(:last_failure_date) { 1.month.ago.iso8601 }
+
+ before do
+ Gitlab::Redis::SharedState.with { |r| r.set('web_hooks:last_failure:minimal_test_class-1', last_failure_date) }
+ end
+
+ it { is_expected.to eq(last_failure_date) }
+ end
+ end
+end
diff --git a/spec/models/container_registry/data_repair_detail_spec.rb b/spec/models/container_registry/data_repair_detail_spec.rb
new file mode 100644
index 00000000000..92833553a1e
--- /dev/null
+++ b/spec/models/container_registry/data_repair_detail_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::DataRepairDetail, type: :model, feature_category: :container_registry do
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.new(project: project) }
+
+ it { is_expected.to belong_to(:project).required }
+end
diff --git a/spec/models/container_registry/event_spec.rb b/spec/models/container_registry/event_spec.rb
index 07ac35f7b6a..edec5afaddb 100644
--- a/spec/models/container_registry/event_spec.rb
+++ b/spec/models/container_registry/event_spec.rb
@@ -225,6 +225,28 @@ RSpec.describe ContainerRegistry::Event do
end
end
+ context 'when it is a manifest delete event' do
+ let(:raw_event) { { 'action' => 'delete', 'target' => { 'digest' => 'x' }, 'actor' => {} } }
+
+ it 'calls the ContainerRegistryEventCounter' do
+ expect(::Gitlab::UsageDataCounters::ContainerRegistryEventCounter)
+ .to receive(:count).with('i_container_registry_delete_manifest')
+
+ subject
+ end
+ end
+
+ context 'when it is not a manifest delete event' do
+ let(:raw_event) { { 'action' => 'push', 'target' => { 'digest' => 'x' }, 'actor' => {} } }
+
+ it 'does not call the ContainerRegistryEventCounter' do
+ expect(::Gitlab::UsageDataCounters::ContainerRegistryEventCounter)
+ .not_to receive(:count).with('i_container_registry_delete_manifest')
+
+ subject
+ end
+ end
+
context 'without an actor name' do
let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'personal_access_token' } } }
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index da7b54644bd..e028a82ff76 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -528,6 +528,10 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#each_tags_page' do
let(:page_size) { 100 }
+ before do
+ allow(repository).to receive(:migrated?).and_return(true)
+ end
+
shared_examples 'iterating through a page' do |expected_tags: true|
it 'iterates through one page' do
expect(repository.gitlab_api_client).to receive(:tags)
@@ -660,7 +664,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
context 'calling on a non migrated repository' do
before do
- repository.update!(created_at: described_class::MIGRATION_PHASE_1_ENDED_AT - 3.days)
+ allow(repository).to receive(:migrated?).and_return(false)
end
it 'raises an Argument error' do
@@ -795,6 +799,7 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
freeze_time do
expect { subject }
.to change { repository.expiration_policy_started_at }.from(nil).to(Time.zone.now)
+ .and change { repository.expiration_policy_cleanup_status }.from('cleanup_unscheduled').to('cleanup_ongoing')
.and change { repository.last_cleanup_deleted_tags_count }.from(10).to(nil)
end
end
@@ -1236,6 +1241,8 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
end
before do
+ allow(group).to receive(:first_project_with_container_registry_tags).and_return(nil)
+
group.parent = test_group
group.save!
end
@@ -1561,22 +1568,20 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
describe '#migrated?' do
subject { repository.migrated? }
- it { is_expected.to eq(true) }
-
- context 'with a created_at older than phase 1 ends' do
+ context 'on gitlab.com' do
before do
- repository.update!(created_at: described_class::MIGRATION_PHASE_1_ENDED_AT - 3.days)
+ allow(::Gitlab).to receive(:com?).and_return(true)
end
- it { is_expected.to eq(false) }
-
- context 'with migration state set to import_done' do
- before do
- repository.update!(migration_state: 'import_done')
- end
+ it { is_expected.to eq(true) }
+ end
- it { is_expected.to eq(true) }
+ context 'not on gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
end
+
+ it { is_expected.to eq(false) }
end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 57e0d1cad8b..cb122fa09fc 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -55,6 +55,7 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_presence_of(:filename) }
it { is_expected.to validate_length_of(:filename).is_at_most(255) }
+ it { is_expected.to validate_length_of(:description).is_at_most(Gitlab::Database::MAX_TEXT_SIZE_LIMIT) }
it { is_expected.to validate_uniqueness_of(:filename).scoped_to(:issue_id) }
it "validates that the extension is an image" do
@@ -512,11 +513,11 @@ RSpec.describe DesignManagement::Design, feature_category: :design_management do
end
describe '#to_reference' do
+ let(:filename) { 'homescreen.jpg' }
let(:namespace) { build(:namespace, id: non_existing_record_id, path: 'sample-namespace') }
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
- let(:group) { create(:group, name: 'Group', path: 'sample-group') }
+ let(:group) { build(:group, name: 'Group', path: 'sample-group') }
let(:issue) { build(:issue, iid: 1, project: project) }
- let(:filename) { 'homescreen.jpg' }
let(:design) { build(:design, filename: filename, issue: issue, project: project) }
context 'when nil argument' 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 26795c0ea7f..34be6ec7fa9 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
+RSpec.describe ErrorTracking::ProjectErrorTrackingSetting, feature_category: :error_tracking do
include ReactiveCachingHelpers
include Gitlab::Routing
@@ -93,9 +93,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
context 'with sentry backend' do
- before do
- subject.integrated = false
- end
+ subject { build(:project_error_tracking_setting, project: project) }
it 'does not create a new client key' do
expect { subject.save! }.not_to change { ErrorTracking::ClientKey.count }
@@ -302,7 +300,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
it { expect(result[:issue].gitlab_commit_path).to eq(nil) }
end
- context 'when repo commit matches first release version' do
+ context 'when repo commit matches first relase version' do
let(:commit) { instance_double(Commit, id: commit_id) }
let(:repository) { instance_double(Repository, commit: commit) }
@@ -341,18 +339,19 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
describe '#update_issue' do
let(:result) { subject.update_issue(**opts) }
- let(:opts) { { issue_id: 1, params: {} } }
+ let(:issue_id) { 1 }
+ let(:opts) { { issue_id: issue_id, params: {} } }
before do
allow(subject).to receive(:sentry_client).and_return(sentry_client)
allow(sentry_client).to receive(:issue_details)
- .with({ issue_id: 1 })
+ .with({ issue_id: issue_id })
.and_return(Gitlab::ErrorTracking::DetailedError.new(project_id: sentry_project_id))
end
context 'when sentry response is successful' do
before do
- allow(sentry_client).to receive(:update_issue).with(opts).and_return(true)
+ allow(sentry_client).to receive(:update_issue).with(**opts).and_return(true)
end
it 'returns the successful response' do
@@ -362,7 +361,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
context 'when sentry raises an error' do
before do
- allow(sentry_client).to receive(:update_issue).with(opts).and_raise(StandardError)
+ allow(sentry_client).to receive(:update_issue).with(**opts).and_raise(StandardError)
end
it 'returns the successful response' do
@@ -391,7 +390,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
setting.update!(sentry_project_id: nil)
allow(sentry_client).to receive(:projects).and_return(sentry_projects)
- allow(sentry_client).to receive(:update_issue).with(opts).and_return(true)
+ allow(sentry_client).to receive(:update_issue).with(**opts).and_return(true)
end
it 'tries to backfill it from sentry API' do
@@ -419,6 +418,25 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
end
+
+ describe 'passing parameters to sentry client' do
+ include SentryClientHelpers
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" }
+ let(:token) { 'test-token' }
+ let(:sentry_client) { ErrorTracking::SentryClient.new(sentry_url, token) }
+
+ before do
+ stub_sentry_request(sentry_request_url, :put, body: true)
+
+ allow(sentry_client).to receive(:update_issue).and_call_original
+ end
+
+ it 'returns the successful response' do
+ expect(result).to eq(updated: true)
+ end
+ end
end
describe 'slugs' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0a05c558d45..3134f2ba248 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -40,7 +40,19 @@ RSpec.describe Group, feature_category: :subgroups do
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::GroupDistribution').dependent(:destroy) }
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
+
+ it do
+ is_expected.to have_many(:application_setting)
+ .with_foreign_key(:instance_administrators_group_id).inverse_of(:instance_group)
+ end
+
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
+
+ it do
+ is_expected.to have_many(:bulk_import_entities).class_name('BulkImports::Entity')
+ .with_foreign_key(:namespace_id).inverse_of(:group)
+ end
+
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) }
@@ -448,6 +460,8 @@ RSpec.describe Group, feature_category: :subgroups do
it_behaves_like 'a BulkUsersByEmailLoad model'
+ it_behaves_like 'ensures runners_token is prefixed', :group
+
context 'after initialized' do
it 'has a group_feature' do
expect(described_class.new.group_feature).to be_present
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 72958a54e10..8fd76d2a835 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -318,7 +318,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'is 10 minutes' do
- expect(hook.next_backoff).to eq(described_class::INITIAL_BACKOFF)
+ expect(hook.next_backoff).to eq(WebHooks::AutoDisabling::INITIAL_BACKOFF)
end
end
@@ -328,7 +328,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'is twice the initial value' do
- expect(hook.next_backoff).to eq(2 * described_class::INITIAL_BACKOFF)
+ expect(hook.next_backoff).to eq(2 * WebHooks::AutoDisabling::INITIAL_BACKOFF)
end
end
@@ -338,7 +338,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'grows exponentially' do
- expect(hook.next_backoff).to eq(2 * 2 * 2 * described_class::INITIAL_BACKOFF)
+ expect(hook.next_backoff).to eq(2 * 2 * 2 * WebHooks::AutoDisabling::INITIAL_BACKOFF)
end
end
@@ -348,7 +348,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'does not exceed the max backoff value' do
- expect(hook.next_backoff).to eq(described_class::MAX_BACKOFF)
+ expect(hook.next_backoff).to eq(WebHooks::AutoDisabling::MAX_BACKOFF)
end
end
end
@@ -470,13 +470,13 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'reduces to MAX_FAILURES' do
- expect { hook.backoff! }.to change(hook, :recent_failures).to(described_class::MAX_FAILURES)
+ expect { hook.backoff! }.to change(hook, :recent_failures).to(WebHooks::AutoDisabling::MAX_FAILURES)
end
end
context 'when the recent failure value is MAX_FAILURES' do
before do
- hook.update!(recent_failures: described_class::MAX_FAILURES, disabled_until: 1.hour.ago)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::MAX_FAILURES, disabled_until: 1.hour.ago)
end
it 'does not change recent_failures' do
@@ -486,7 +486,7 @@ RSpec.describe WebHook, feature_category: :integrations do
context 'when we have exhausted the grace period' do
before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
it 'sets disabled_until to the next backoff' do
@@ -499,8 +499,8 @@ RSpec.describe WebHook, feature_category: :integrations do
context 'when we have backed off MAX_FAILURES times' do
before do
- stub_const("#{described_class}::MAX_FAILURES", 5)
- (described_class::FAILURE_THRESHOLD + 5).times { hook.backoff! }
+ stub_const("WebHooks::AutoDisabling::MAX_FAILURES", 5)
+ (WebHooks::AutoDisabling::FAILURE_THRESHOLD + 5).times { hook.backoff! }
end
it 'does not let the backoff count exceed the maximum failure count' do
@@ -516,7 +516,7 @@ RSpec.describe WebHook, feature_category: :integrations do
it 'changes disabled_until when it has elapsed', :skip_freeze_time do
travel_to(hook.disabled_until + 1.minute) do
expect { hook.backoff! }.to change { hook.disabled_until }
- expect(hook.backoff_count).to eq(described_class::MAX_FAILURES)
+ expect(hook.backoff_count).to eq(WebHooks::AutoDisabling::MAX_FAILURES)
end
end
end
@@ -539,7 +539,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
it 'does not update the hook if the the failure count exceeds the maximum value' do
- hook.recent_failures = described_class::MAX_FAILURES
+ hook.recent_failures = WebHooks::AutoDisabling::MAX_FAILURES
sql_count = ActiveRecord::QueryRecorder.new { hook.failed! }.count
diff --git a/spec/models/import_export_upload_spec.rb b/spec/models/import_export_upload_spec.rb
index e13f504b82a..9811dbf60e3 100644
--- a/spec/models/import_export_upload_spec.rb
+++ b/spec/models/import_export_upload_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe ImportExportUpload do
let(:after_commit_callbacks) { described_class._commit_callbacks.select { |cb| cb.kind == :after } }
def find_callback(callbacks, key)
- callbacks.find { |cb| cb.instance_variable_get(:@key) == key }
+ callbacks.find { |cb| cb.filter == key }
end
it 'export file is stored in after_commit callback' do
diff --git a/spec/models/import_failure_spec.rb b/spec/models/import_failure_spec.rb
index 9fee1b0ae7b..0bdcb6dde31 100644
--- a/spec/models/import_failure_spec.rb
+++ b/spec/models/import_failure_spec.rb
@@ -10,7 +10,11 @@ RSpec.describe ImportFailure do
let_it_be(:soft_failure) { create(:import_failure, :soft_failure, project: project, correlation_id_value: correlation_id) }
let_it_be(:unrelated_failure) { create(:import_failure, project: project) }
- it 'returns hard failures given a correlation ID' do
+ it 'returns failures for the given correlation ID' do
+ expect(ImportFailure.failures_by_correlation_id(correlation_id)).to match_array([hard_failure, soft_failure])
+ end
+
+ it 'returns hard failures for the given correlation ID' do
expect(ImportFailure.hard_failures_by_correlation_id(correlation_id)).to eq([hard_failure])
end
@@ -45,5 +49,15 @@ RSpec.describe ImportFailure do
it { is_expected.to validate_presence_of(:group) }
end
+
+ describe '#external_identifiers' do
+ it { is_expected.to allow_value({ note_id: 234, noteable_id: 345, noteable_type: 'MergeRequest' }).for(:external_identifiers) }
+ it { is_expected.not_to allow_value(nil).for(:external_identifiers) }
+ it { is_expected.not_to allow_value({ ids: [123] }).for(:external_identifiers) }
+
+ it 'allows up to 3 fields' do
+ is_expected.not_to allow_value({ note_id: 234, noteable_id: 345, noteable_type: 'MergeRequest', extra_attribute: 'abc' }).for(:external_identifiers)
+ end
+ end
end
end
diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb
index 1a57f556895..b2a52c8aaf0 100644
--- a/spec/models/integrations/apple_app_store_spec.rb
+++ b/spec/models/integrations/apple_app_store_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
it { is_expected.to validate_presence_of :app_store_issuer_id }
it { is_expected.to validate_presence_of :app_store_key_id }
it { is_expected.to validate_presence_of :app_store_private_key }
+ it { is_expected.to validate_presence_of :app_store_private_key_file_name }
it { is_expected.to allow_value('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee').for(:app_store_issuer_id) }
it { is_expected.not_to allow_value('abcde').for(:app_store_issuer_id) }
it { is_expected.to allow_value(File.read('spec/fixtures/ssl_key.pem')).for(:app_store_private_key) }
@@ -28,8 +29,8 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
describe '#fields' do
it 'returns custom fields' do
- expect(apple_app_store_integration.fields.pluck(:name)).to eq(%w[app_store_issuer_id app_store_key_id
- app_store_private_key])
+ expect(apple_app_store_integration.fields.pluck(:name)).to match_array(%w[app_store_issuer_id app_store_key_id
+ app_store_private_key app_store_private_key_file_name])
end
end
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
index ae923cd38fc..38d3d89cdbf 100644
--- a/spec/models/integrations/campfire_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Campfire do
+RSpec.describe Integrations::Campfire, feature_category: :integrations do
include StubRequests
it_behaves_like Integrations::ResetSecretFields do
@@ -88,4 +88,16 @@ RSpec.describe Integrations::Campfire do
expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
end
end
+
+ describe '#log_error' do
+ subject { described_class.new.log_error('error') }
+
+ it 'logs an error' do
+ expect(Gitlab::IntegrationsLogger).to receive(:error).with(
+ hash_including(integration_class: 'Integrations::Campfire', message: 'error')
+ ).and_call_original
+
+ is_expected.to be_truthy
+ end
+ end
end
diff --git a/spec/models/integrations/google_play_spec.rb b/spec/models/integrations/google_play_spec.rb
new file mode 100644
index 00000000000..ab1aaad24e7
--- /dev/null
+++ b/spec/models/integrations/google_play_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GooglePlay, feature_category: :mobile_devops do
+ describe 'Validations' do
+ context 'when active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :service_account_key_file_name }
+ it { is_expected.to validate_presence_of :service_account_key }
+ it { is_expected.to allow_value(File.read('spec/fixtures/service_account.json')).for(:service_account_key) }
+ it { is_expected.not_to allow_value(File.read('spec/fixtures/group.json')).for(:service_account_key) }
+ end
+ end
+
+ context 'when integration is enabled' do
+ let(:google_play_integration) { build(:google_play_integration) }
+
+ describe '#fields' do
+ it 'returns custom fields' do
+ expect(google_play_integration.fields.pluck(:name)).to match_array(%w[service_account_key
+ service_account_key_file_name])
+ end
+ end
+
+ describe '#test' do
+ it 'returns true for a successful request' do
+ allow(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds, :fetch_access_token!)
+ expect(google_play_integration.test[:success]).to be true
+ end
+
+ it 'returns false for an invalid request' do
+ allow(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds,
+ :fetch_access_token!).and_raise(Signet::AuthorizationError.new('error'))
+ expect(google_play_integration.test[:success]).to be false
+ end
+ end
+
+ describe '#help' do
+ it 'renders prompt information' do
+ expect(google_play_integration.help).not_to be_empty
+ end
+ end
+
+ describe '.to_param' do
+ it 'returns the name of the integration' do
+ expect(described_class.to_param).to eq('google_play')
+ end
+ end
+
+ describe '#ci_variables' do
+ let(:google_play_integration) { build_stubbed(:google_play_integration) }
+
+ it 'returns vars when the integration is activated' do
+ ci_vars = [
+ {
+ key: 'SUPPLY_JSON_KEY_DATA',
+ value: google_play_integration.service_account_key,
+ masked: true,
+ public: false
+ }
+ ]
+
+ expect(google_play_integration.ci_variables).to match_array(ci_vars)
+ end
+ end
+ end
+
+ context 'when integration is disabled' do
+ let(:google_play_integration) { build_stubbed(:google_play_integration, active: false) }
+
+ describe '#ci_variables' do
+ it 'returns an empty array' do
+ expect(google_play_integration.ci_variables).to match_array([])
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index a4ccae459cf..fad8768cba0 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -590,7 +590,6 @@ RSpec.describe Integrations::Jira do
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject { close_issue }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { 'Integrations::Jira' }
let(:action) { 'perform_integrations_action' }
let(:namespace) { project.namespace }
@@ -944,7 +943,6 @@ RSpec.describe Integrations::Jira do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { 'Integrations::Jira' }
let(:action) { 'perform_integrations_action' }
let(:namespace) { project.namespace }
@@ -1095,9 +1093,7 @@ RSpec.describe Integrations::Jira do
expect(integration.web_url).to eq('')
end
- it 'includes Atlassian referrer for gitlab.com' do
- allow(Gitlab).to receive(:com?).and_return(true)
-
+ it 'includes Atlassian referrer for SaaS', :saas do
expect(integration.web_url).to eq("http://jira.test.com/path?#{described_class::ATLASSIAN_REFERRER_GITLAB_COM.to_query}")
allow(Gitlab).to receive(:staging?).and_return(true)
diff --git a/spec/models/integrations/mattermost_slash_commands_spec.rb b/spec/models/integrations/mattermost_slash_commands_spec.rb
index 070adb9ba93..e393a905f45 100644
--- a/spec/models/integrations/mattermost_slash_commands_spec.rb
+++ b/spec/models/integrations/mattermost_slash_commands_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::MattermostSlashCommands do
+RSpec.describe Integrations::MattermostSlashCommands, feature_category: :integrations do
it_behaves_like Integrations::BaseSlashCommands
describe 'Mattermost API' do
@@ -123,12 +123,5 @@ RSpec.describe Integrations::MattermostSlashCommands do
end
end
end
-
- describe '#chat_responder' do
- it 'returns the responder to use for Mattermost' do
- expect(described_class.new.chat_responder)
- .to eq(Gitlab::Chat::Responder::Mattermost)
- end
- end
end
end
diff --git a/spec/models/integrations/slack_slash_commands_spec.rb b/spec/models/integrations/slack_slash_commands_spec.rb
index 22cbaa777cd..f373fc2a2de 100644
--- a/spec/models/integrations/slack_slash_commands_spec.rb
+++ b/spec/models/integrations/slack_slash_commands_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::SlackSlashCommands do
+RSpec.describe Integrations::SlackSlashCommands, feature_category: :integrations do
it_behaves_like Integrations::BaseSlashCommands
describe '#trigger' do
@@ -40,11 +40,4 @@ RSpec.describe Integrations::SlackSlashCommands do
end
end
end
-
- describe '#chat_responder' do
- it 'returns the responder to use for Slack' do
- expect(described_class.new.chat_responder)
- .to eq(Gitlab::Chat::Responder::Slack)
- end
- end
end
diff --git a/spec/models/integrations/squash_tm_spec.rb b/spec/models/integrations/squash_tm_spec.rb
new file mode 100644
index 00000000000..f12e37dae6d
--- /dev/null
+++ b/spec/models/integrations/squash_tm_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SquashTm, feature_category: :integrations do
+ it_behaves_like Integrations::HasWebHook do
+ let_it_be(:project) { create(:project) }
+
+ let(:integration) { build(:squash_tm_integration, project: project) }
+ let(:hook_url) { "#{integration.url}?token={token}" }
+ end
+
+ it_behaves_like Integrations::ResetSecretFields do
+ let(:integration) { subject }
+ end
+
+ describe 'Validations' do
+ context 'when integration is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to allow_value('https://example.com').for(:url) }
+ it { is_expected.not_to allow_value(nil).for(:url) }
+ it { is_expected.not_to allow_value('').for(:url) }
+ it { is_expected.not_to allow_value('foo').for(:url) }
+ it { is_expected.not_to allow_value('example.com').for(:url) }
+
+ it { is_expected.not_to validate_presence_of(:token) }
+ it { is_expected.to validate_length_of(:token).is_at_most(255) }
+ it { is_expected.to allow_value(nil).for(:token) }
+ it { is_expected.to allow_value('foo').for(:token) }
+ end
+
+ context 'when integration is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:url) }
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe '#execute' do
+ let(:integration) { build(:squash_tm_integration, project: build(:project)) }
+
+ let(:squash_tm_hook_url) do
+ "#{integration.url}?token=#{integration.token}"
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+ let(:data) { issue.to_hook_data(user) }
+
+ before do
+ stub_request(:post, squash_tm_hook_url)
+ end
+
+ it 'calls Squash TM API' do
+ integration.execute(data)
+
+ expect(a_request(:post, squash_tm_hook_url)).to have_been_made.once
+ end
+ end
+
+ describe '#test' do
+ let(:integration) { build(:squash_tm_integration) }
+
+ let(:squash_tm_hook_url) do
+ "#{integration.url}?token=#{integration.token}"
+ end
+
+ subject(:result) { integration.test({}) }
+
+ context 'when server is responding' do
+ let(:body) { 'OK' }
+ let(:status) { 200 }
+
+ before do
+ stub_request(:post, squash_tm_hook_url)
+ .to_return(status: status, body: body)
+ end
+
+ it { is_expected.to eq(success: true, result: 'OK') }
+ end
+
+ context 'when server rejects the request' do
+ let(:body) { 'Unauthorized' }
+ let(:status) { 401 }
+
+ before do
+ stub_request(:post, squash_tm_hook_url)
+ .to_return(status: status, body: body)
+ end
+
+ it { is_expected.to eq(success: false, result: body) }
+ end
+
+ context 'when test request executes with errors' do
+ before do
+ allow(integration).to receive(:execute_web_hook!)
+ .with({}, "Test Configuration Hook")
+ .and_raise(StandardError, 'error message')
+ end
+
+ it { is_expected.to eq(success: false, result: 'error message') }
+ end
+ end
+
+ describe '.default_test_event' do
+ subject { described_class.default_test_event }
+
+ it { is_expected.to eq('issue') }
+ end
+end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index f0007e1203c..59ade8783e5 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe InternalId do
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:id_subject) { issue }
- let(:scope) { { project: project } }
+ let(:scope) { { namespace: project.project_namespace } }
let(:init) { ->(issue, scope) { issue&.project&.issues&.size || Issue.where(**scope).count } }
it_behaves_like 'having unique enum values'
@@ -17,7 +17,7 @@ RSpec.describe InternalId do
end
describe '.flush_records!' do
- subject { described_class.flush_records!(project: project) }
+ subject { described_class.flush_records!(namespace: project.project_namespace) }
let(:another_project) { create(:project) }
@@ -27,11 +27,11 @@ RSpec.describe InternalId do
end
it 'deletes all records for the given project' do
- expect { subject }.to change { described_class.where(project: project).count }.from(1).to(0)
+ expect { subject }.to change { described_class.where(namespace: project.project_namespace).count }.from(1).to(0)
end
it 'retains records for other projects' do
- expect { subject }.not_to change { described_class.where(project: another_project).count }
+ expect { subject }.not_to change { described_class.where(namespace: another_project.project_namespace).count }
end
it 'does not allow an empty filter' do
@@ -51,7 +51,7 @@ RSpec.describe InternalId do
subject
described_class.first.tap do |record|
- expect(record.project).to eq(project)
+ expect(record.namespace).to eq(project.project_namespace)
expect(record.usage).to eq(usage.to_s)
end
end
@@ -182,7 +182,7 @@ RSpec.describe InternalId do
subject
described_class.first.tap do |record|
- expect(record.project).to eq(project)
+ expect(record.namespace).to eq(project.project_namespace)
expect(record.usage).to eq(usage.to_s)
expect(record.last_value).to eq(value)
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e29318a7e83..8072a60326c 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -63,13 +63,18 @@ RSpec.describe Issue, feature_category: :team_planning do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:issue) }
- let(:scope) { :project }
- let(:scope_attrs) { { project: instance.project } }
+ let(:scope) { :namespace }
+ let(:scope_attrs) { { namespace: instance.project.project_namespace } }
let(:usage) { :issues }
end
end
describe 'validations' do
+ it { is_expected.not_to allow_value(nil).for(:confidential) }
+ it { is_expected.to allow_value(true, false).for(:confidential) }
+ end
+
+ describe 'custom validations' do
subject(:valid?) { issue.valid? }
describe 'due_date_after_start_date' do
@@ -215,7 +220,7 @@ RSpec.describe Issue, feature_category: :team_planning do
subject { create(:issue, project: reusable_project) }
describe 'callbacks' do
- describe '#ensure_metrics' do
+ describe '#ensure_metrics!' do
it 'creates metrics after saving' do
expect(subject.metrics).to be_persisted
expect(Issue::Metrics.count).to eq(1)
@@ -575,38 +580,38 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe '#to_reference' do
- let(:namespace) { build(:namespace, path: 'sample-namespace') }
- let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
- let(:issue) { build(:issue, iid: 1, project: project) }
+ let_it_be(:namespace) { create(:namespace, path: 'sample-namespace') }
+ let_it_be(:project) { create(:project, name: 'sample-project', namespace: namespace) }
+ let_it_be(:issue) { create(:issue, project: project) }
context 'when nil argument' do
it 'returns issue id' do
- expect(issue.to_reference).to eq "#1"
+ expect(issue.to_reference).to eq "##{issue.iid}"
end
it 'returns complete path to the issue with full: true' do
- expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
context 'when argument is a project' do
context 'when same project' do
it 'returns issue id' do
- expect(issue.to_reference(project)).to eq("#1")
+ expect(issue.to_reference(project)).to eq("##{issue.iid}")
end
it 'returns full reference with full: true' do
- expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(project, full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
context 'when cross-project in same namespace' do
let(:another_project) do
- build(:project, name: 'another-project', namespace: project.namespace)
+ create(:project, name: 'another-project', namespace: project.namespace)
end
it 'returns a cross-project reference' do
- expect(issue.to_reference(another_project)).to eq "sample-project#1"
+ expect(issue.to_reference(another_project)).to eq "sample-project##{issue.iid}"
end
end
@@ -615,7 +620,7 @@ RSpec.describe Issue, feature_category: :team_planning do
let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) }
it 'returns complete path to the issue' do
- expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(another_namespace_project)).to eq "#{project.full_path}##{issue.iid}"
end
end
end
@@ -623,11 +628,11 @@ RSpec.describe Issue, feature_category: :team_planning do
context 'when argument is a namespace' do
context 'when same as issue' do
it 'returns path to the issue with the project name' do
- expect(issue.to_reference(namespace)).to eq 'sample-project#1'
+ expect(issue.to_reference(namespace)).to eq "sample-project##{issue.iid}"
end
it 'returns full reference with full: true' do
- expect(issue.to_reference(namespace, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(namespace, full: true)).to eq "#{project.full_path}##{issue.iid}"
end
end
@@ -635,12 +640,111 @@ RSpec.describe Issue, feature_category: :team_planning do
let(:group) { build(:group, name: 'Group', path: 'sample-group') }
it 'returns full path to the issue with full: true' do
- expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(group)).to eq "#{project.full_path}##{issue.iid}"
end
end
end
end
+ describe '#to_reference with table syntax' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project.project_namespace }
+ let_it_be(:same_namespace_project) { create(:project, namespace: group) }
+ let_it_be(:same_namespace_project_namespace) { same_namespace_project.project_namespace }
+
+ let_it_be(:another_namespace_project) { create(:project) }
+ let_it_be(:another_namespace_project_namespace) { another_namespace_project.project_namespace }
+
+ let_it_be(:project_issue) { build(:issue, project: project, iid: 123) }
+ let_it_be(:project_issue_full_reference) { "#{project.full_path}##{project_issue.iid}" }
+
+ let_it_be(:group_issue) { build(:issue, namespace: group, iid: 123) }
+ let_it_be(:group_issue_full_reference) { "#{group.full_path}##{group_issue.iid}" }
+
+ # this one is just theoretically possible, not smth to be supported for real
+ let_it_be(:user_issue) { build(:issue, namespace: user_namespace, iid: 123) }
+ let_it_be(:user_issue_full_reference) { "#{user_namespace.full_path}##{user_issue.iid}" }
+
+ # namespace would be group, project namespace or user namespace
+ where(:issue, :full, :from, :result) do
+ ref(:project_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | nil | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:group) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:parent) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:parent) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:project) | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | ref(:project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:project_namespace) | lazy { "##{issue.iid}" }
+ ref(:project_issue) | true | ref(:project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:same_namespace_project) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:same_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:same_namespace_project_namespace) | lazy { "#{project.path}##{issue.iid}" }
+ ref(:project_issue) | true | ref(:same_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_group) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_namespace_project) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:another_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:another_namespace_project_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | false | ref(:user_namespace) | ref(:project_issue_full_reference)
+ ref(:project_issue) | true | ref(:user_namespace) | ref(:project_issue_full_reference)
+
+ ref(:group_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:group_issue) | true | nil | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:user_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:user_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:group) | lazy { "##{issue.iid}" }
+ ref(:group_issue) | true | ref(:group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:parent) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:parent) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:project) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:project_namespace) | lazy { "#{group.path}##{issue.iid}" }
+ ref(:group_issue) | true | ref(:project_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_group) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_namespace_project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_namespace_project) | ref(:group_issue_full_reference)
+ ref(:group_issue) | false | ref(:another_namespace_project_namespace) | ref(:group_issue_full_reference)
+ ref(:group_issue) | true | ref(:another_namespace_project_namespace) | ref(:group_issue_full_reference)
+
+ ref(:user_issue) | false | nil | lazy { "##{issue.iid}" }
+ ref(:user_issue) | true | nil | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:user_namespace) | lazy { "##{issue.iid}" }
+ ref(:user_issue) | true | ref(:user_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:parent) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:parent) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_group) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_namespace_project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_namespace_project) | ref(:user_issue_full_reference)
+ ref(:user_issue) | false | ref(:another_namespace_project_namespace) | ref(:user_issue_full_reference)
+ ref(:user_issue) | true | ref(:another_namespace_project_namespace) | ref(:user_issue_full_reference)
+ end
+
+ with_them do
+ it 'returns correct reference' do
+ expect(issue.to_reference(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe '#assignee_or_author?' do
let(:issue) { create(:issue, project: reusable_project) }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6a52f12553f..30607116c61 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -16,7 +16,6 @@ RSpec.describe Member, feature_category: :subgroups do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:member_namespace) }
- it { is_expected.to belong_to(:member_role) }
it { is_expected.to have_one(:member_task) }
end
@@ -173,96 +172,6 @@ RSpec.describe Member, feature_category: :subgroups do
end
end
end
-
- context 'member role access level' do
- let_it_be_with_reload(:member) { create(:group_member, access_level: Gitlab::Access::DEVELOPER) }
-
- context 'when no member role is associated' do
- it 'is valid' do
- expect(member).to be_valid
- end
- end
-
- context 'when member role is associated' do
- let!(:member_role) do
- create(
- :member_role,
- members: [member],
- base_access_level: Gitlab::Access::DEVELOPER,
- namespace: member.member_namespace
- )
- end
-
- context 'when member role matches access level' do
- it 'is valid' do
- expect(member).to be_valid
- end
- end
-
- context 'when member role does not match access level' do
- it 'is invalid' do
- member_role.base_access_level = Gitlab::Access::MAINTAINER
-
- expect(member).not_to be_valid
- end
- end
-
- context 'when access_level is changed' do
- it 'is invalid' do
- member.access_level = Gitlab::Access::MAINTAINER
-
- expect(member).not_to be_valid
- expect(member.errors[:access_level]).to include(
- _("cannot be changed since member is associated with a custom role")
- )
- end
- end
- end
- end
-
- context 'member role namespace' do
- let_it_be_with_reload(:member) { create(:group_member) }
-
- context 'when no member role is associated' do
- it 'is valid' do
- expect(member).to be_valid
- end
- end
-
- context 'when member role is associated' do
- let_it_be(:member_role) do
- create(:member_role, members: [member], namespace: member.group, base_access_level: member.access_level)
- end
-
- context 'when member#member_namespace is a group within hierarchy of member_role#namespace' do
- it 'is valid' do
- member.member_namespace = create(:group, parent: member_role.namespace)
-
- expect(member).to be_valid
- end
- end
-
- context 'when member#member_namespace is a project within hierarchy of member_role#namespace' do
- it 'is valid' do
- project = create(:project, group: member_role.namespace)
- member.member_namespace = Namespace.find(project.parent_id)
-
- expect(member).to be_valid
- end
- end
-
- context 'when member#member_namespace is outside hierarchy of member_role#namespace' do
- it 'is invalid' do
- member.member_namespace = create(:group)
-
- expect(member).not_to be_valid
- expect(member.errors[:member_namespace]).to include(
- _("must be in same hierarchy as custom role's namespace")
- )
- end
- end
- end
- end
end
describe 'Scopes & finders' do
@@ -774,6 +683,24 @@ RSpec.describe Member, feature_category: :subgroups do
end
end
+ describe '.filter_by_user_type' do
+ let_it_be(:service_account) { create(:user, :service_account) }
+ let_it_be(:service_account_member) { create(:group_member, user: service_account) }
+ let_it_be(:other_member) { create(:group_member) }
+
+ context 'when the user type is valid' do
+ it 'returns service accounts' do
+ expect(described_class.filter_by_user_type('service_account')).to match_array([service_account_member])
+ end
+ end
+
+ context 'when the user type is invalid' do
+ it 'returns nil' do
+ expect(described_class.filter_by_user_type('invalid_type')).to eq(nil)
+ end
+ end
+ end
+
describe '#accept_request' do
let(:member) { create(:project_member, requested_at: Time.current.utc) }
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
deleted file mode 100644
index 4bf33eb1fce..00000000000
--- a/spec/models/members/member_role_spec.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MemberRole, feature_category: :authentication_and_authorization do
- describe 'associations' do
- it { is_expected.to belong_to(:namespace) }
- it { is_expected.to have_many(:members) }
- end
-
- describe 'validation' do
- subject(:member_role) { build(:member_role) }
-
- it { is_expected.to validate_presence_of(:namespace) }
- it { is_expected.to validate_presence_of(:base_access_level) }
-
- context 'for attributes_locked_after_member_associated' do
- context 'when assigned to member' do
- it 'cannot be changed' do
- member_role.save!
- member_role.members << create(:project_member)
-
- expect(member_role).not_to be_valid
- expect(member_role.errors.messages[:base]).to include(
- s_("MemberRole|cannot be changed because it is already assigned to a user. "\
- "Please create a new Member Role instead")
- )
- end
- end
-
- context 'when not assigned to member' do
- it 'can be changed' do
- expect(member_role).to be_valid
- end
- end
- end
-
- context 'when for namespace' do
- let_it_be(:root_group) { create(:group) }
-
- context 'when namespace is a subgroup' do
- it 'is invalid' do
- subgroup = create(:group, parent: root_group)
- member_role.namespace = subgroup
-
- expect(member_role).to be_invalid
- expect(member_role.errors[:namespace]).to include(
- s_("MemberRole|must be top-level namespace")
- )
- end
- end
-
- context 'when namespace is a root group' do
- it 'is valid' do
- member_role.namespace = root_group
-
- expect(member_role).to be_valid
- end
- end
-
- context 'when namespace is not present' do
- it 'is invalid with a different error message' do
- member_role.namespace = nil
-
- expect(member_role).to be_invalid
- expect(member_role.errors[:namespace]).to include(_("can't be blank"))
- end
- end
-
- context 'when namespace is outside hierarchy of member' do
- it 'creates a validation error' do
- member_role.save!
- member_role.namespace = create(:group)
-
- expect(member_role).not_to be_valid
- expect(member_role.errors[:namespace]).to include(s_("MemberRole|can't be changed"))
- end
- end
- end
- end
-
- describe 'callbacks' do
- context 'for preventing deletion after member is associated' do
- let_it_be(:member_role) { create(:member_role) }
-
- subject(:destroy_member_role) { member_role.destroy } # rubocop: disable Rails/SaveBang
-
- it 'allows deletion without any member associated' do
- expect(destroy_member_role).to be_truthy
- end
-
- it 'prevent deletion when member is associated' do
- create(:group_member, { group: member_role.namespace,
- access_level: Gitlab::Access::DEVELOPER,
- member_role: member_role })
- member_role.members.reload
-
- expect(destroy_member_role).to be_falsey
- expect(member_role.errors.messages[:base])
- .to(
- include(s_("MemberRole|cannot be deleted because it is already assigned to a user. "\
- "Please disassociate the member role from all users before deletion."))
- )
- end
- end
- end
-end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index f0069b89494..f2bc9b42b77 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe ProjectMember do
describe 'validations' do
it { is_expected.to allow_value('Project').for(:source_type) }
- it { is_expected.not_to allow_value('project').for(:source_type) }
+ it { is_expected.not_to allow_value('Group').for(:source_type) }
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 2e2355ba710..b82b16fdec3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -20,6 +20,11 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
+
+ it do
+ is_expected.to belong_to(:head_pipeline).class_name('Ci::Pipeline').inverse_of(:merge_requests_as_head_pipeline)
+ end
+
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:reviewers).through(:merge_request_reviewers) }
it { is_expected.to have_many(:merge_request_diffs) }
@@ -393,8 +398,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
instance1 = MergeRequest.find(merge_request.id)
instance2 = MergeRequest.find(merge_request.id)
- instance1.ensure_metrics
- instance2.ensure_metrics
+ instance1.ensure_metrics!
+ instance2.ensure_metrics!
metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
expect(metrics_records.size).to eq(1)
@@ -760,6 +765,23 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
+ context 'when scoped with :merged_before and :merged_after' do
+ before do
+ mr2.metrics.update!(merged_at: mr1.metrics.merged_at - 1.week)
+ mr3.metrics.update!(merged_at: mr1.metrics.merged_at + 1.week)
+ end
+
+ it 'excludes merge requests outside of the date range' do
+ expect(
+ project.merge_requests.merge(
+ MergeRequest::Metrics
+ .merged_before(mr1.metrics.merged_at + 1.day)
+ .merged_after(mr1.metrics.merged_at - 1.day)
+ ).total_time_to_merge
+ ).to be_within(1).of(expected_total_time([mr1]))
+ end
+ end
+
def expected_total_time(mrs)
mrs = mrs.reject { |mr| mr.merged_at.nil? }
mrs.reduce(0.0) do |sum, mr|
@@ -4274,8 +4296,11 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it 'refreshes the number of open merge requests of the target project' do
project = subject.target_project
- expect { subject.destroy! }
- .to change { project.open_merge_requests_count }.from(1).to(0)
+ expect do
+ subject.destroy!
+
+ BatchLoader::Executor.clear_current
+ end.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
@@ -4301,6 +4326,14 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
transition!
end
+ context 'when skip_merge_status_trigger is set to true' do
+ before do
+ subject.skip_merge_status_trigger = true
+ end
+
+ it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription'
+ end
+
context 'when transaction is not committed' do
it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription' do
def transition!
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index a0698ac30f5..89df24d75c9 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it { is_expected.to have_many :pending_builds }
it { is_expected.to have_one :namespace_route }
it { is_expected.to have_many :namespace_members }
- it { is_expected.to have_many :member_roles }
it { is_expected.to have_one :cluster_enabled_grant }
it { is_expected.to have_many(:work_items) }
it { is_expected.to have_many :achievements }
@@ -173,15 +172,24 @@ RSpec.describe Namespace, feature_category: :subgroups do
# rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
where(:namespace_type, :path, :valid) do
- ref(:project_sti_name) | 'j' | true
- ref(:project_sti_name) | 'path.' | true
- ref(:project_sti_name) | 'blob' | false
- ref(:group_sti_name) | 'j' | false
- ref(:group_sti_name) | 'path.' | false
- ref(:group_sti_name) | 'blob' | true
- ref(:user_sti_name) | 'j' | false
- ref(:user_sti_name) | 'path.' | false
- ref(:user_sti_name) | 'blob' | true
+ ref(:project_sti_name) | 'j' | true
+ ref(:project_sti_name) | 'path.' | false
+ ref(:project_sti_name) | '.path' | false
+ ref(:project_sti_name) | 'path.git' | false
+ ref(:project_sti_name) | 'namespace__path' | false
+ ref(:project_sti_name) | 'blob' | false
+ ref(:group_sti_name) | 'j' | false
+ ref(:group_sti_name) | 'path.' | false
+ ref(:group_sti_name) | '.path' | false
+ ref(:group_sti_name) | 'path.git' | false
+ ref(:group_sti_name) | 'namespace__path' | false
+ ref(:group_sti_name) | 'blob' | true
+ ref(:user_sti_name) | 'j' | false
+ ref(:user_sti_name) | 'path.' | false
+ ref(:user_sti_name) | '.path' | false
+ ref(:user_sti_name) | 'path.git' | false
+ ref(:user_sti_name) | 'namespace__path' | false
+ ref(:user_sti_name) | 'blob' | true
end
# rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
@@ -193,6 +201,26 @@ RSpec.describe Namespace, feature_category: :subgroups do
expect(namespace.valid?).to be(valid)
end
end
+
+ context 'when path starts or ends with a special character' do
+ it 'does not raise validation error for path for existing namespaces' do
+ parent.update_attribute(:path, '_path_')
+
+ expect { parent.update!(name: 'Foo') }.not_to raise_error
+ end
+ end
+
+ context 'when restrict_special_characters_in_namespace_path feature flag is disabled' do
+ before do
+ stub_feature_flags(restrict_special_characters_in_namespace_path: false)
+ end
+
+ it 'allows special character at the start or end of project namespace path' do
+ namespace = build(:namespace, type: project_sti_name, parent: parent, path: '_path_')
+
+ expect(namespace).to be_valid
+ end
+ end
end
describe '1 char path length' do
@@ -237,6 +265,117 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
+ describe "ReferencePatternValidation" do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match("@group1") }
+ it { is_expected.to match("@group1/group2/group3") }
+ it { is_expected.to match("@1234/1234/1234") }
+ it { is_expected.to match("@.q-w_e") }
+ end
+
+ describe '#to_reference_base' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project.project_namespace }
+
+ let_it_be(:another_namespace_project) { create(:project) }
+ let_it_be(:another_namespace_project_namespace) { another_namespace_project.project_namespace }
+
+ # testing references with namespace being: group, project namespace and user namespace
+ where(:namespace, :full, :from, :result) do
+ ref(:parent) | false | nil | nil
+ ref(:parent) | true | nil | lazy { parent.full_path }
+ ref(:parent) | false | ref(:group) | lazy { parent.path }
+ ref(:parent) | true | ref(:group) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:parent) | nil
+ ref(:parent) | true | ref(:parent) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:project) | lazy { parent.path }
+ ref(:parent) | true | ref(:project) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:project_namespace) | lazy { parent.path }
+ ref(:parent) | true | ref(:project_namespace) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_group) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_group) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_namespace_project) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_namespace_project) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:another_namespace_project_namespace) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:another_namespace_project_namespace) | lazy { parent.full_path }
+ ref(:parent) | false | ref(:user_namespace) | lazy { parent.full_path }
+ ref(:parent) | true | ref(:user_namespace) | lazy { parent.full_path }
+
+ ref(:group) | false | nil | nil
+ ref(:group) | true | nil | lazy { group.full_path }
+ ref(:group) | false | ref(:group) | nil
+ ref(:group) | true | ref(:group) | lazy { group.full_path }
+ ref(:group) | false | ref(:parent) | lazy { group.path }
+ ref(:group) | true | ref(:parent) | lazy { group.full_path }
+ ref(:group) | false | ref(:project) | lazy { group.path }
+ ref(:group) | true | ref(:project) | lazy { group.full_path }
+ ref(:group) | false | ref(:project_namespace) | lazy { group.path }
+ ref(:group) | true | ref(:project_namespace) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_group) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_group) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_namespace_project) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_namespace_project) | lazy { group.full_path }
+ ref(:group) | false | ref(:another_namespace_project_namespace) | lazy { group.full_path }
+ ref(:group) | true | ref(:another_namespace_project_namespace) | lazy { group.full_path }
+ ref(:group) | false | ref(:user_namespace) | lazy { group.full_path }
+ ref(:group) | true | ref(:user_namespace) | lazy { group.full_path }
+
+ ref(:project_namespace) | false | nil | nil
+ ref(:project_namespace) | true | nil | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:group) | lazy { project_namespace.path }
+ ref(:project_namespace) | true | ref(:group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:parent) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:parent) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:project) | nil
+ ref(:project_namespace) | true | ref(:project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:project_namespace) | nil
+ ref(:project_namespace) | true | ref(:project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_group) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_namespace_project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_namespace_project) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:another_namespace_project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:another_namespace_project_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | false | ref(:user_namespace) | lazy { project_namespace.full_path }
+ ref(:project_namespace) | true | ref(:user_namespace) | lazy { project_namespace.full_path }
+
+ ref(:user_namespace) | false | nil | nil
+ ref(:user_namespace) | true | nil | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:user_namespace) | nil
+ ref(:user_namespace) | true | ref(:user_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:parent) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:parent) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_group) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_namespace_project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_namespace_project) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | false | ref(:another_namespace_project_namespace) | lazy { user_namespace.full_path }
+ ref(:user_namespace) | true | ref(:another_namespace_project_namespace) | lazy { user_namespace.full_path }
+ end
+
+ with_them do
+ it 'returns correct path' do
+ expect(namespace.to_reference_base(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe 'handling STI', :aggregate_failures do
let(:namespace_type) { nil }
let(:parent) { nil }
@@ -436,6 +575,14 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
+ context 'when parent is nil' do
+ let(:namespace) { build(:group, parent: nil) }
+
+ it 'returns []' do
+ expect(namespace.traversal_ids).to eq []
+ end
+ end
+
context 'when made a child group' do
let!(:namespace) { create(:group) }
let!(:parent_namespace) { create(:group, children: [namespace]) }
@@ -458,6 +605,17 @@ RSpec.describe Namespace, feature_category: :subgroups do
expect(namespace.root_ancestor).to eq new_root
end
end
+
+ context 'within a transaction' do
+ # We would like traversal_ids to be defined within a transaction, but it's not possible yet.
+ # This spec exists to assert that the behavior is known.
+ it 'is not defined yet' do
+ Namespace.transaction do
+ group = create(:group)
+ expect(group.traversal_ids).to be_empty
+ end
+ end
+ end
end
context 'traversal scopes' do
@@ -542,17 +700,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it { expect(child.traversal_ids).to eq [parent.id, child.id] }
it { expect(parent.sync_events.count).to eq 1 }
it { expect(child.sync_events.count).to eq 1 }
-
- context 'when set_traversal_ids_on_save feature flag is disabled' do
- before do
- stub_feature_flags(set_traversal_ids_on_save: false)
- end
-
- it 'only sets traversal_ids on reload' do
- expect { parent.reload }.to change(parent, :traversal_ids).from([]).to([parent.id])
- expect { child.reload }.to change(child, :traversal_ids).from([]).to([parent.id, child.id])
- end
- end
end
context 'traversal_ids on update' do
@@ -565,18 +712,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
it 'sets the traversal_ids attribute' do
expect { subject }.to change { namespace1.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
end
-
- context 'when set_traversal_ids_on_save feature flag is disabled' do
- before do
- stub_feature_flags(set_traversal_ids_on_save: false)
- end
-
- it 'sets traversal_ids after reload' do
- subject
-
- expect { namespace1.reload }.to change(namespace1, :traversal_ids).from([]).to([namespace2.id, namespace1.id])
- end
- end
end
it 'creates a Namespaces::SyncEvent using triggers' do
@@ -677,24 +812,41 @@ RSpec.describe Namespace, feature_category: :subgroups do
describe '#any_project_has_container_registry_tags?' do
subject { namespace.any_project_has_container_registry_tags? }
- let!(:project_without_registry) { create(:project, namespace: namespace) }
+ let(:project) { create(:project, namespace: namespace) }
- context 'without tags' do
- it { is_expected.to be_falsey }
+ it 'returns true if there is a project with container registry tags' do
+ expect(namespace).to receive(:first_project_with_container_registry_tags).and_return(project)
+
+ expect(subject).to be_truthy
end
- context 'with tags' do
- before do
- repositories = create_list(:container_repository, 3)
- create(:project, namespace: namespace, container_repositories: repositories)
+ it 'returns false if there is no project with container registry tags' do
+ expect(namespace).to receive(:first_project_with_container_registry_tags).and_return(nil)
+ expect(subject).to be_falsey
+ end
+ end
+
+ describe '#first_project_with_container_registry_tags' do
+ let(:container_repository) { create(:container_repository) }
+ let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
+
+ context 'when Gitlab API is not supported' do
+ before do
stub_container_registry_config(enabled: true)
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(false)
end
- it 'finds tags' do
+ it 'returns the project' do
stub_container_registry_tags(repository: :any, tags: ['tag'])
- is_expected.to be_truthy
+ expect(namespace.first_project_with_container_registry_tags).to eq(project)
+ end
+
+ it 'returns no project' do
+ stub_container_registry_tags(repository: :any, tags: nil)
+
+ expect(namespace.first_project_with_container_registry_tags).to be_nil
end
it 'does not cause N+1 query in fetching registries' do
@@ -704,29 +856,52 @@ RSpec.describe Namespace, feature_category: :subgroups do
other_repositories = create_list(:container_repository, 2)
create(:project, namespace: namespace, container_repositories: other_repositories)
- expect { namespace.any_project_has_container_registry_tags? }.not_to exceed_query_limit(control_count + 1)
+ expect { namespace.first_project_with_container_registry_tags }.not_to exceed_query_limit(control_count + 1)
end
end
- end
- describe '#first_project_with_container_registry_tags' do
- let(:container_repository) { create(:container_repository) }
- let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
+ context 'when Gitlab API is supported' do
+ before do
+ allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true)
+ stub_container_registry_config(enabled: true, api_url: 'http://container-registry', key: 'spec/fixtures/x509_certificate_pk.key')
+ end
- before do
- stub_container_registry_config(enabled: true)
- end
+ it 'calls and returns GitlabApiClient.one_project_with_container_registry_tag' do
+ expect(ContainerRegistry::GitlabApiClient)
+ .to receive(:one_project_with_container_registry_tag)
+ .with(namespace.full_path)
+ .and_return(project)
- it 'returns the project' do
- stub_container_registry_tags(repository: :any, tags: ['tag'])
+ expect(namespace.first_project_with_container_registry_tags).to eq(project)
+ end
- expect(namespace.first_project_with_container_registry_tags).to eq(project)
- end
+ context 'when the feature flag use_sub_repositories_api is disabled' do
+ before do
+ stub_feature_flags(use_sub_repositories_api: false)
+ end
+
+ it 'returns the project' do
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+
+ expect(namespace.first_project_with_container_registry_tags).to eq(project)
+ end
+
+ it 'returns no project' do
+ stub_container_registry_tags(repository: :any, tags: nil)
+
+ expect(namespace.first_project_with_container_registry_tags).to be_nil
+ end
- it 'returns no project' do
- stub_container_registry_tags(repository: :any, tags: nil)
+ it 'does not cause N+1 query in fetching registries' do
+ stub_container_registry_tags(repository: :any, tags: [])
+ control_count = ActiveRecord::QueryRecorder.new { namespace.any_project_has_container_registry_tags? }.count
- expect(namespace.first_project_with_container_registry_tags).to be_nil
+ other_repositories = create_list(:container_repository, 2)
+ create(:project, namespace: namespace, container_repositories: other_repositories)
+
+ expect { namespace.first_project_with_container_registry_tags }.not_to exceed_query_limit(control_count + 1)
+ end
+ end
end
end
@@ -755,6 +930,7 @@ RSpec.describe Namespace, feature_category: :subgroups do
with_them do
before do
+ allow(ContainerRegistry::GitlabApiClient).to receive(:one_project_with_container_registry_tag).and_return(nil)
stub_container_registry_config(enabled: true, api_url: 'http://container-registry', key: 'spec/fixtures/x509_certificate_pk.key')
allow(Gitlab).to receive(:com?).and_return(true)
allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(gitlab_api_supported)
@@ -975,43 +1151,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '.find_by_pages_host' do
- it 'finds namespace by GitLab Pages host and is case-insensitive' do
- namespace = create(:namespace, name: 'topNAMEspace', path: 'topNAMEspace')
- create(:namespace, name: 'annother_namespace')
- host = "TopNamespace.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to eq(namespace)
- end
-
- context 'when there is non-top-level group with searched name' do
- before do
- create(:group, :nested, path: 'pages')
- end
-
- it 'ignores this group' do
- host = "pages.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to be_nil
- end
-
- it 'finds right top level group' do
- group = create(:group, path: 'pages')
-
- host = "pages.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to eq(group)
- end
- end
-
- it "returns no result if the provided host is not subdomain of the Pages host" do
- create(:namespace, name: 'namespace.io')
- host = "namespace.io"
-
- expect(described_class.find_by_pages_host(host)).to eq(nil)
- end
- end
-
describe '.top_most' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:group) { create(:group) }
@@ -1037,6 +1176,7 @@ RSpec.describe Namespace, feature_category: :subgroups do
allow(namespace).to receive(:path_was).and_return(namespace.path)
allow(namespace).to receive(:path).and_return('new_path')
+ allow(namespace).to receive(:first_project_with_container_registry_tags).and_return(project)
end
it 'raises an error about not movable project' do
@@ -1917,6 +2057,62 @@ RSpec.describe Namespace, feature_category: :subgroups do
expect(very_deep_nested_group.root_ancestor).to eq(root_group)
end
end
+
+ context 'when parent is changed' do
+ let(:group) { create(:group) }
+ let(:new_parent) { create(:group) }
+
+ shared_examples 'updates root_ancestor' do
+ it do
+ expect { subject }.to change { group.root_ancestor }.from(group).to(new_parent)
+ end
+ end
+
+ context 'by object' do
+ subject { group.parent = new_parent }
+
+ include_examples 'updates root_ancestor'
+ end
+
+ context 'by id' do
+ subject { group.parent_id = new_parent.id }
+
+ include_examples 'updates root_ancestor'
+ end
+ end
+
+ context 'within a transaction' do
+ context 'with a persisted parent' do
+ let(:parent) { create(:group) }
+
+ it do
+ Namespace.transaction do
+ group = create(:group, parent: parent)
+ expect(group.root_ancestor).to eq parent
+ end
+ end
+ end
+
+ context 'with a non-persisted parent' do
+ let(:parent) { build(:group) }
+
+ it do
+ Namespace.transaction do
+ group = create(:group, parent: parent)
+ expect(group.root_ancestor).to eq parent
+ end
+ end
+ end
+
+ context 'without a parent' do
+ it do
+ Namespace.transaction do
+ group = create(:group)
+ expect(group.root_ancestor).to eq group
+ end
+ end
+ end
+ end
end
describe '#full_path_before_last_save' do
@@ -2105,34 +2301,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '#pages_virtual_domain' do
- let(:project) { create(:project, namespace: namespace) }
- let(:virtual_domain) { namespace.pages_virtual_domain }
-
- before do
- project.mark_pages_as_deployed
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- it 'returns the virual domain' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{namespace.root_ancestor.id}_/)
- end
-
- context 'when :cache_pages_domain_api is disabled' do
- before do
- stub_feature_flags(cache_pages_domain_api: false)
- end
-
- it 'returns the virual domain' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to be_nil
- end
- end
- end
-
describe '#any_project_with_pages_deployed?' do
it 'returns true if any project nested under the group has pages deployed' do
parent_1 = create(:group) # Three projects, one with pages
diff --git a/spec/models/namespaces/randomized_suffix_path_spec.rb b/spec/models/namespaces/randomized_suffix_path_spec.rb
index a2484030f3c..fc5ccd95ce6 100644
--- a/spec/models/namespaces/randomized_suffix_path_spec.rb
+++ b/spec/models/namespaces/randomized_suffix_path_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::RandomizedSuffixPath, feature_category: :not_owned do
+RSpec.describe Namespaces::RandomizedSuffixPath, feature_category: :shared do
let(:path) { 'backintime' }
subject(:suffixed_path) { described_class.new(path) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 013070f7be5..c1de8125c0d 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1100,6 +1100,16 @@ RSpec.describe Note do
end
end
+ describe '#for_work_item?' do
+ it 'returns true for a work item' do
+ expect(build(:note_on_work_item).for_work_item?).to be true
+ end
+
+ it 'returns false for an issue' do
+ expect(build(:note_on_issue).for_work_item?).to be false
+ end
+ end
+
describe '#for_project_snippet?' do
it 'returns true for a project snippet note' do
expect(build(:note_on_project_snippet).for_project_snippet?).to be true
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
index fc53d926dd6..5fa590eab58 100644
--- a/spec/models/oauth_access_token_spec.rb
+++ b/spec/models/oauth_access_token_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe OauthAccessToken do
it 'uses the expires_in value' do
token = OauthAccessToken.new(expires_in: 1.minute)
- expect(token.expires_in).to eq 1.minute
+ expect(token).to be_valid
end
end
@@ -67,7 +67,7 @@ RSpec.describe OauthAccessToken do
it 'uses default value' do
token = OauthAccessToken.new(expires_in: nil)
- expect(token.expires_in).to eq 2.hours
+ expect(token).to be_invalid
end
end
end
diff --git a/spec/models/onboarding/completion_spec.rb b/spec/models/onboarding/completion_spec.rb
index e1fad4255bc..0639762b76c 100644
--- a/spec/models/onboarding/completion_spec.rb
+++ b/spec/models/onboarding/completion_spec.rb
@@ -2,24 +2,24 @@
require 'spec_helper'
-RSpec.describe Onboarding::Completion do
+RSpec.describe Onboarding::Completion, feature_category: :onboarding do
+ let(:completed_actions) { {} }
+ let(:project) { build(:project, namespace: namespace) }
+ let!(:onboarding_progress) { create(:onboarding_progress, namespace: namespace, **completed_actions) }
+
+ let_it_be(:namespace) { create(:namespace) }
+
describe '#percentage' do
- let(:completed_actions) { {} }
- 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| ::Onboarding::Progress.column_name(key) }
+ [*described_class::ACTION_PATHS, :security_scan_enabled].map do |key|
+ ::Onboarding::Progress.column_name(key)
+ end
end
- let_it_be(:namespace) { create(:namespace) }
-
- subject { described_class.new(namespace).percentage }
+ subject(:percentage) { described_class.new(project).percentage }
context 'when no onboarding_progress exists' do
- subject { described_class.new(build(:namespace)).percentage }
+ subject(:percentage) { described_class.new(build(:project)).percentage }
it { is_expected.to eq(0) }
end
@@ -29,6 +29,8 @@ RSpec.describe Onboarding::Completion do
end
context 'when all tracked actions have been completed' do
+ let(:project) { build(:project, :stubbed_commit_count, namespace: namespace) }
+
let(:completed_actions) do
tracked_action_columns.index_with { Time.current }
end
@@ -44,7 +46,7 @@ RSpec.describe Onboarding::Completion do
stub_experiments(security_actions_continuous_onboarding: :control)
end
- it { is_expected.to eq(11) }
+ it { is_expected.to eq(10) }
end
context 'when candidate' do
@@ -52,7 +54,50 @@ RSpec.describe Onboarding::Completion do
stub_experiments(security_actions_continuous_onboarding: :candidate)
end
- it { is_expected.to eq(9) }
+ it { is_expected.to eq(8) }
+ end
+ end
+ end
+
+ describe '#completed?' do
+ subject(:completed?) { described_class.new(project).completed?(column) }
+
+ context 'when code_added' do
+ let(:column) { :code_added }
+
+ context 'when commit_count > 1' do
+ let(:project) { build(:project, :stubbed_commit_count, namespace: namespace) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when branch_count > 1' do
+ let(:project) { build(:project, :stubbed_branch_count, namespace: namespace) }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when empty repository' do
+ let(:project) { build(:project, namespace: namespace) }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when security_scan_enabled' do
+ let(:column) { :security_scan_enabled_at }
+ let(:completed_actions) { { security_scan_enabled_at: security_scan_enabled_at } }
+
+ context 'when is completed' do
+ let(:security_scan_enabled_at) { Time.current }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when is not completed' do
+ let(:security_scan_enabled_at) { nil }
+
+ it { is_expected.to eq(false) }
end
end
end
diff --git a/spec/models/packages/debian/file_metadatum_spec.rb b/spec/models/packages/debian/file_metadatum_spec.rb
index 1215adfa6a1..8cbd83c3e2d 100644
--- a/spec/models/packages/debian/file_metadatum_spec.rb
+++ b/spec/models/packages/debian/file_metadatum_spec.rb
@@ -79,6 +79,7 @@ RSpec.describe Packages::Debian::FileMetadatum, type: :model do
:debian_package_file | :dsc | true | false | true
:debian_package_file | :deb | true | true | true
:debian_package_file | :udeb | true | true | true
+ :debian_package_file | :ddeb | true | true | true
:debian_package_file | :buildinfo | true | false | true
:debian_package_file | :changes | false | false | true
end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 9b341034aaa..d80f8247261 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -153,6 +153,7 @@ RSpec.describe Packages::PackageFile, type: :model do
let_it_be(:debian_changes) { debian_package.package_files.last }
let_it_be(:debian_deb) { create(:debian_package_file, package: debian_package) }
let_it_be(:debian_udeb) { create(:debian_package_file, :udeb, package: debian_package) }
+ let_it_be(:debian_ddeb) { create(:debian_package_file, :ddeb, package: debian_package) }
let_it_be(:debian_contrib) do
create(:debian_package_file, package: debian_package).tap do |pf|
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index ef79ba28d5d..38ff1bb090e 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -61,15 +61,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses deployment from object storage' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: deployment.file.url(expire_at: 1.day.from_now),
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -87,15 +85,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses file protocol' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: 'file://' + deployment.file.path,
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: "file://#{deployment.file.path}",
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -108,15 +104,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses deployment from object storage' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: deployment.file.url(expire_at: 1.day.from_now),
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -143,4 +137,25 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
expect(lookup_path.prefix).to eq('/myproject/')
end
end
+
+ describe '#unique_domain' do
+ let(:project) { build(:project) }
+
+ context 'when unique domain is disabled' do
+ it 'returns nil' do
+ project.project_setting.pages_unique_domain_enabled = false
+
+ expect(lookup_path.unique_domain).to be_nil
+ end
+ end
+
+ context 'when unique domain is enabled' do
+ it 'returns the project unique domain' do
+ project.project_setting.pages_unique_domain_enabled = true
+ project.project_setting.pages_unique_domain = 'unique-domain'
+
+ expect(lookup_path.unique_domain).to eq('unique-domain')
+ end
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index f054fde78e7..b218d4dce09 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe PagesDomain do
describe 'associations' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:serverless_domain_clusters) }
end
describe '.for_project' do
@@ -546,44 +545,6 @@ RSpec.describe PagesDomain do
end
end
- describe '#pages_virtual_domain' do
- let(:project) { create(:project) }
- let(:pages_domain) { create(:pages_domain, project: project) }
-
- context 'when there are no pages deployed for the project' do
- it 'returns nil' do
- expect(pages_domain.pages_virtual_domain).to be_nil
- end
- end
-
- context 'when there are pages deployed for the project' do
- let(:virtual_domain) { pages_domain.pages_virtual_domain }
-
- before do
- project.mark_pages_as_deployed
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- it 'returns the virual domain when there are pages deployed for the project' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/)
- end
-
- context 'when :cache_pages_domain_api is disabled' do
- before do
- stub_feature_flags(cache_pages_domain_api: false)
- end
-
- it 'returns the virual domain when there are pages deployed for the project' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to be_nil
- end
- end
- end
- end
-
describe '#validate_custom_domain_count_per_project' do
let_it_be(:project) { create(:project) }
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 2320ff669d0..45b8de509fc 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessToken, feature_category: :authentication_and_authorization do
+RSpec.describe PersonalAccessToken, feature_category: :system_access do
subject { described_class }
describe '.build' do
@@ -405,4 +405,26 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
end
end
end
+
+ describe 'token format' do
+ let(:personal_access_token) { described_class.new }
+
+ it 'generates a token' do
+ expect { personal_access_token.ensure_token }
+ .to change { personal_access_token.token }.from(nil).to(a_string_starting_with(described_class.token_prefix))
+ end
+
+ context 'when there is an existing token' do
+ let(:token) { 'an_existing_secret_token' }
+
+ before do
+ personal_access_token.set_token(token)
+ end
+
+ it 'does not change the existing token' do
+ expect { personal_access_token.ensure_token }
+ .not_to change { personal_access_token.token }.from(token)
+ end
+ end
+ end
end
diff --git a/spec/models/preloaders/runner_machine_policy_preloader_spec.rb b/spec/models/preloaders/runner_machine_policy_preloader_spec.rb
new file mode 100644
index 00000000000..26fc101d8dc
--- /dev/null
+++ b/spec/models/preloaders/runner_machine_policy_preloader_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::RunnerMachinePolicyPreloader, feature_category: :runner_fleet do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:runner1) { create(:ci_runner) }
+ let_it_be(:runner2) { create(:ci_runner) }
+ let_it_be(:runner_machine1) { create(:ci_runner_machine, runner: runner1) }
+ let_it_be(:runner_machine2) { create(:ci_runner_machine, runner: runner2) }
+
+ let(:base_runner_machines) do
+ Project.where(id: [runner_machine1, runner_machine2])
+ end
+
+ it 'avoids N+1 queries when authorizing a list of runner machines', :request_store do
+ preload_runner_machines_for_policy(user)
+ control = ActiveRecord::QueryRecorder.new { authorize_all_runner_machines(user) }
+
+ new_runner1 = create(:ci_runner)
+ new_runner2 = create(:ci_runner)
+ new_runner_machine1 = create(:ci_runner_machine, runner: new_runner1)
+ new_runner_machine2 = create(:ci_runner_machine, runner: new_runner2)
+
+ pristine_runner_machines = Project.where(id: base_runner_machines + [new_runner_machine1, new_runner_machine2])
+
+ preload_runner_machines_for_policy(user, pristine_runner_machines)
+ expect { authorize_all_runner_machines(user, pristine_runner_machines) }.not_to exceed_query_limit(control)
+ end
+
+ def authorize_all_runner_machines(current_user, runner_machine_list = base_runner_machines)
+ runner_machine_list.each { |runner_machine| current_user.can?(:read_runner_machine, runner_machine) }
+ end
+
+ def preload_runner_machines_for_policy(current_user, runner_machine_list = base_runner_machines)
+ described_class.new(runner_machine_list, current_user).execute
+ end
+end
diff --git a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
index 7d04817b621..3fba2ac003b 100644
--- a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
@@ -66,22 +66,6 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
create(:group_group_link, :guest, shared_with_group: group1, shared_group: group4)
end
- context 'when `include_memberships_from_group_shares_in_preloader` feature flag is disabled' do
- before do
- stub_feature_flags(include_memberships_from_group_shares_in_preloader: false)
- end
-
- it 'sets access_level to `NO_ACCESS` in cache for groups arising from group shares' do
- described_class.new(groups, user).execute
-
- groups.each do |group|
- cached_access_level = group.max_member_access_for_user(user)
-
- expect(cached_access_level).to eq(Gitlab::Access::NO_ACCESS)
- end
- end
- end
-
it 'sets the right access level in cache for groups arising from group shares' do
described_class.new(groups, user).execute
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 2c490c33747..0a818147bfc 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -27,22 +27,8 @@ RSpec.describe ProjectCiCdSetting do
end
end
- describe '#set_default_for_inbound_job_token_scope_enabled' do
- context 'when feature flag ci_inbound_job_token_scope is enabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: true)
- end
-
- it { is_expected.to be_inbound_job_token_scope_enabled }
- end
-
- context 'when feature flag ci_inbound_job_token_scope is disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- it { is_expected.not_to be_inbound_job_token_scope_enabled }
- end
+ describe '#default_for_inbound_job_token_scope_enabled' do
+ it { is_expected.to be_inbound_job_token_scope_enabled }
end
describe '#default_git_depth' do
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index fe0b46c3117..5da6a66b3ae 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -314,6 +314,40 @@ RSpec.describe ProjectFeature, feature_category: :projects do
end
end
+ describe '#public_packages?' do
+ let_it_be(:public_project) { create(:project, :public) }
+
+ context 'with packages config enabled' do
+ context 'when project is private' do
+ it 'returns false' do
+ expect(project.project_feature.public_packages?).to eq(false)
+ end
+
+ context 'with package_registry_access_level set to public' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it 'returns true' do
+ expect(project.project_feature.public_packages?).to eq(true)
+ end
+ end
+ end
+
+ context 'when project is public' do
+ it 'returns true' do
+ expect(public_project.project_feature.public_packages?).to eq(true)
+ end
+ end
+ end
+
+ it 'returns false if packages config is not enabled' do
+ stub_config(packages: { enabled: false })
+
+ expect(public_project.project_feature.public_packages?).to eq(false)
+ end
+ end
+
# rubocop:disable Gitlab/FeatureAvailableUsage
describe '#feature_available?' do
let(:features) { ProjectFeature::FEATURES }
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index feb5985818b..42433a2a84a 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -39,6 +39,44 @@ RSpec.describe ProjectSetting, type: :model do
[nil, 'not_allowed', :invalid].each do |invalid_value|
it { is_expected.not_to allow_value([invalid_value]).for(:target_platforms) }
end
+
+ context "when pages_unique_domain is required", feature_category: :pages do
+ it "is not required if pages_unique_domain_enabled is false" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: false)
+
+ expect(project_setting).to be_valid
+ expect(project_setting.errors.full_messages).not_to include("Pages unique domain can't be blank")
+ end
+
+ it "is required when pages_unique_domain_enabled is true" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: true)
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+
+ it "is required if it is already saved in the database" do
+ project_setting = create(
+ :project_setting,
+ pages_unique_domain: "random-unique-domain-here",
+ pages_unique_domain_enabled: true
+ )
+
+ project_setting.pages_unique_domain = nil
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+ end
+
+ it "validates uniqueness of pages_unique_domain", feature_category: :pages do
+ create(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ project_setting = build(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain has already been taken")
+ end
end
describe 'target_platforms=' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index dfc8919e19d..15e5db5af60 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'ensures runners_token is prefixed', :project
+
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
@@ -50,6 +52,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }
it { is_expected.to have_one(:apple_app_store_integration) }
+ it { is_expected.to have_one(:google_play_integration) }
it { is_expected.to have_one(:asana_integration) }
it { is_expected.to have_many(:boards) }
it { is_expected.to have_one(:campfire_integration) }
@@ -88,6 +91,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') }
it { is_expected.to have_one(:mock_ci_integration) }
it { is_expected.to have_one(:mock_monitoring_integration) }
+ it { is_expected.to have_one(:service_desk_custom_email_verification).class_name('ServiceDesk::CustomEmailVerification') }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:ci_pipelines) }
it { is_expected.to have_many(:ci_refs) }
@@ -135,6 +139,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:reviews).inverse_of(:project) }
it { is_expected.to have_many(:packages).class_name('Packages::Package') }
it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') }
+ it { is_expected.to have_many(:rpm_repository_files).class_name('Packages::Rpm::RepositoryFile').inverse_of(:project).dependent(:destroy) }
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) }
it { is_expected.to have_one(:packages_cleanup_policy).class_name('Packages::Cleanup::Policy').inverse_of(:project) }
it { is_expected.to have_many(:pipeline_artifacts).dependent(:restrict_with_error) }
@@ -939,6 +944,17 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#commit_notes' do
+ let_it_be(:project) { create(:project) }
+
+ it "returns project's commit notes" do
+ note_1 = create(:note_on_commit, project: project, commit_id: 'commit_id_1')
+ note_2 = create(:note_on_commit, project: project, commit_id: 'commit_id_2')
+
+ expect(project.commit_notes).to match_array([note_1, note_2])
+ end
+ end
+
describe '#personal_namespace_holder?' do
let_it_be(:group) { create(:group) }
let_it_be(:namespace_user) { create(:user) }
@@ -1151,7 +1167,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#ci_inbound_job_token_scope_enabled?' do
- it_behaves_like 'a ci_cd_settings predicate method', prefix: 'ci_' do
+ it_behaves_like 'a ci_cd_settings predicate method', prefix: 'ci_', default: true do
let(:delegated_method) { :inbound_job_token_scope_enabled? }
end
end
@@ -1380,6 +1396,60 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#to_reference_base' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_namespace) { user.namespace }
+
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent) }
+ let_it_be(:another_group) { create(:group) }
+
+ let_it_be(:project1) { create(:project, namespace: group) }
+ let_it_be(:project_namespace) { project1.project_namespace }
+
+ # different project same group
+ let_it_be(:project2) { create(:project, namespace: group) }
+ let_it_be(:project_namespace2) { project2.project_namespace }
+
+ # different project from different group
+ let_it_be(:project3) { create(:project) }
+ let_it_be(:project_namespace3) { project3.project_namespace }
+
+ # testing references with namespace being: group, project namespace and user namespace
+ where(:project, :full, :from, :result) do
+ ref(:project1) | false | nil | nil
+ ref(:project1) | true | nil | lazy { project.full_path }
+ ref(:project1) | false | ref(:group) | lazy { project.path }
+ ref(:project1) | true | ref(:group) | lazy { project.full_path }
+ ref(:project1) | false | ref(:parent) | lazy { project.full_path }
+ ref(:project1) | true | ref(:parent) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project1) | nil
+ ref(:project1) | true | ref(:project1) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace) | nil
+ ref(:project1) | true | ref(:project_namespace) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project2) | lazy { project.path }
+ ref(:project1) | true | ref(:project2) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace2) | lazy { project.path }
+ ref(:project1) | true | ref(:project_namespace2) | lazy { project.full_path }
+ ref(:project1) | false | ref(:another_group) | lazy { project.full_path }
+ ref(:project1) | true | ref(:another_group) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project3) | lazy { project.full_path }
+ ref(:project1) | true | ref(:project3) | lazy { project.full_path }
+ ref(:project1) | false | ref(:project_namespace3) | lazy { project.full_path }
+ ref(:project1) | true | ref(:project_namespace3) | lazy { project.full_path }
+ ref(:project1) | false | ref(:user_namespace) | lazy { project.full_path }
+ ref(:project1) | true | ref(:user_namespace) | lazy { project.full_path }
+ end
+
+ with_them do
+ it 'returns correct path' do
+ expect(project.to_reference_base(from, full: full)).to eq(result)
+ end
+ end
+ end
+
describe '#merge_method' do
where(:ff, :rebase, :method) do
true | true | :ff
@@ -2666,7 +2736,11 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#pages_url', feature_category: :pages do
+ let(:group_name) { 'group' }
+ let(:project_name) { 'project' }
+
let(:group) { create(:group, name: group_name) }
+ let(:nested_group) { create(:group, parent: group) }
let(:project_path) { project_name.downcase }
let(:project) do
@@ -2689,101 +2763,114 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
.and_return(['http://example.com', port].compact.join(':'))
end
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'when pages_unique_domain feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
- it { is_expected.to eq("http://group.example.com") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
- it { is_expected.to eq("http://group.example.com") }
+ project.project_setting.update!(
+ pages_unique_domain_enabled: pages_unique_domain_enabled,
+ pages_unique_domain: 'unique-domain'
+ )
end
- end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { false }
- it { is_expected.to eq("http://group.example.com/project") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { true }
- it { is_expected.to eq("http://group.example.com/Project") }
+ it { is_expected.to eq('http://unique-domain.example.com') }
end
end
- context 'when there is an explicit port' do
- let(:port) { 3000 }
-
- context 'when not in dev mode' do
- before do
- stub_rails_env('production')
- end
+ context 'with nested group' do
+ let(:project) { create(:project, namespace: nested_group, name: project_name) }
+ let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'group page' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ it { is_expected.to eq(expected_url) }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'project page' do
+ let(:project_name) { 'Project' }
- it { is_expected.to eq('http://group.example.com:3000/Group.example.com') }
- end
- end
+ it { is_expected.to eq(expected_url) }
+ end
+ end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when the project matches its namespace url' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ it { is_expected.to eq('http://group.example.com') }
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'with different group name capitalization' do
+ let(:group_name) { 'Group' }
- it { is_expected.to eq("http://group.example.com:3000/Project") }
- end
- end
+ it { is_expected.to eq("http://group.example.com") }
end
- context 'when in dev mode' do
- before do
- stub_rails_env('development')
- end
-
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'with different project path capitalization' do
+ let(:project_path) { 'Group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000') }
+ it { is_expected.to eq("http://group.example.com") }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'with different project name capitalization' do
+ let(:project_name) { 'Project' }
- it { is_expected.to eq('http://group.example.com:3000') }
- end
- end
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when there is an explicit port' do
+ let(:port) { 3000 }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ context 'when not in dev mode' do
+ before do
+ stub_rails_env('production')
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ end
- it { is_expected.to eq("http://group.example.com:3000/Project") }
+ context 'when in dev mode' do
+ before do
+ stub_rails_env('development')
end
+
+ it { is_expected.to eq('http://group.example.com:3000') }
end
end
end
end
+ describe '#pages_unique_url', feature_category: :pages do
+ let(:project_settings) { create(:project_setting, pages_unique_domain: 'unique-domain') }
+ let(:project) { build(:project, project_setting: project_settings) }
+ let(:domain) { 'example.com' }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://#{domain}")
+ end
+
+ it 'returns the pages unique url' do
+ expect(project.pages_unique_url).to eq('http://unique-domain.example.com')
+ end
+ end
+
describe '#pages_namespace_url', feature_category: :pages do
let(:group) { create(:group, name: group_name) }
let(:project) { create(:project, namespace: group, name: project_name) }
@@ -3565,6 +3652,44 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#beautified_import_status_name' do
+ context 'when import not finished' do
+ it 'returns the right beautified import status' do
+ project = create(:project, :import_started)
+
+ expect(project.beautified_import_status_name).to eq('started')
+ end
+ end
+
+ context 'when import is finished' do
+ context 'when import is partially completed' do
+ it 'returns partially completed' do
+ project = create(:project)
+
+ create(:import_state, project: project, status: 'finished', checksums: {
+ 'fetched' => { 'labels' => 10 },
+ 'imported' => { 'labels' => 9 }
+ })
+
+ expect(project.beautified_import_status_name).to eq('partially completed')
+ end
+ end
+
+ context 'when import is fully completed' do
+ it 'returns completed' do
+ project = create(:project)
+
+ create(:import_state, project: project, status: 'finished', checksums: {
+ 'fetched' => { 'labels' => 10 },
+ 'imported' => { 'labels' => 10 }
+ })
+
+ expect(project.beautified_import_status_name).to eq('completed')
+ end
+ end
+ end
+ end
+
describe '#add_import_job' do
let(:import_jid) { '123' }
@@ -3843,21 +3968,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#ancestors' do
- context 'with linear_project_ancestors feature flag enabled' do
- before do
- stub_feature_flags(linear_project_ancestors: true)
- end
-
- include_examples '#ancestors'
- end
-
- context 'with linear_project_ancestors feature flag disabled' do
- before do
- stub_feature_flags(linear_project_ancestors: false)
- end
-
- include_examples '#ancestors'
- end
+ include_examples '#ancestors'
end
describe '#ancestors_upto' do
@@ -4643,52 +4754,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#pages_url' do
- let(:group) { create(:group, name: 'Group') }
- let(:nested_group) { create(:group, parent: group) }
- let(:domain) { 'Example.com' }
-
- subject { project.pages_url }
-
- before do
- allow(Settings.pages).to receive(:host).and_return(domain)
- allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
- end
-
- context 'top-level group' do
- let(:project) { create(:project, namespace: group, name: project_name) }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq("http://group.example.com") }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq("http://group.example.com/project") }
- end
- end
-
- context 'nested group' do
- let(:project) { create(:project, namespace: nested_group, name: project_name) }
- let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq(expected_url) }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq(expected_url) }
- end
- end
- end
-
describe '#lfs_http_url_to_repo' do
let(:project) { create(:project) }
@@ -6049,7 +6114,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it 'executes hooks which were backed off and are no longer backed off' do
project = create(:project)
hook = create(:project_hook, project: project, push_events: true)
- WebHook::FAILURE_THRESHOLD.succ.times { hook.backoff! }
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.succ.times { hook.backoff! }
expect_any_instance_of(ProjectHook).to receive(:async_execute).once
@@ -7305,20 +7370,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe 'with integrations and chat names' do
- subject { create(:project) }
-
- let(:integration) { create(:integration, project: subject) }
-
- before do
- create_list(:chat_name, 5, integration: integration)
- end
-
- it 'does not remove chat names on removal' do
- expect { subject.destroy! }.not_to change { ChatName.count }
- end
- end
-
describe 'with_issues_or_mrs_available_for_user' do
before do
Project.delete_all
@@ -8842,6 +8893,32 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe 'deprecated project attributes' do
+ where(:project_attr, :project_method, :project_feature_attr) do
+ :wiki_enabled | :wiki_enabled? | :wiki_access_level
+ :builds_enabled | :builds_enabled? | :builds_access_level
+ :merge_requests_enabled | :merge_requests_enabled? | :merge_requests_access_level
+ :issues_enabled | :issues_enabled? | :issues_access_level
+ :snippets_enabled | :snippets_enabled? | :snippets_access_level
+ end
+
+ with_them do
+ it 'delegates the attributes to project feature' do
+ project = Project.new(project_attr => false)
+
+ expect(project.public_send(project_method)).to eq(false)
+ expect(project.project_feature.public_send(project_feature_attr)).to eq(ProjectFeature::DISABLED)
+ end
+
+ it 'sets the default value' do
+ project = Project.new
+
+ expect(project.public_send(project_method)).to eq(true)
+ expect(project.project_feature.public_send(project_feature_attr)).to eq(ProjectFeature::ENABLED)
+ end
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/projects/data_transfer_spec.rb b/spec/models/projects/data_transfer_spec.rb
index 6d3ddbdd74e..ab798185bbb 100644
--- a/spec/models/projects/data_transfer_spec.rb
+++ b/spec/models/projects/data_transfer_spec.rb
@@ -7,6 +7,12 @@ RSpec.describe Projects::DataTransfer, feature_category: :source_code_management
it { expect(subject).to be_valid }
+ # tests DataTransferCounterAttribute with the appropiate attributes
+ it_behaves_like CounterAttribute,
+ %i[repository_egress artifacts_egress packages_egress registry_egress] do
+ let(:model) { create(:project_data_transfer, project: project) }
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }
diff --git a/spec/models/projects/forks/divergence_counts_spec.rb b/spec/models/projects/forks/details_spec.rb
index fd69cc0f3e7..4c0a2e3453a 100644
--- a/spec/models/projects/forks/divergence_counts_spec.rb
+++ b/spec/models/projects/forks/details_spec.rb
@@ -2,24 +2,29 @@
require 'spec_helper'
-RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code_management do
+RSpec.describe Projects::Forks::Details, feature_category: :source_code_management do
+ include ExclusiveLeaseHelpers
include ProjectForksHelper
let_it_be(:user) { create(:user) }
+ let_it_be(:source_repo) { create(:project, :repository, :public).repository }
+ let_it_be(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
- describe '#counts', :use_clean_rails_redis_caching do
- let(:source_repo) { create(:project, :repository, :public).repository }
- let(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
- let(:fork_branch) { 'fork-branch' }
- let(:cache_key) { ['project_forks', fork_repo.project.id, fork_branch, 'divergence_counts'] }
+ let(:fork_branch) { 'fork-branch' }
+ let(:cache_key) { ['project_fork_details', fork_repo.project.id, fork_branch].join(':') }
+ describe '#counts', :use_clean_rails_redis_caching do
def expect_cached_counts(value)
counts = described_class.new(fork_repo.project, fork_branch).counts
ahead, behind = value
expect(counts).to eq({ ahead: ahead, behind: behind })
- cached_value = [source_repo.commit.sha, fork_repo.commit(fork_branch).sha, value]
+ cached_value = {
+ source_sha: source_repo.commit.sha,
+ sha: fork_repo.commit(fork_branch).sha,
+ counts: value
+ }
expect(Rails.cache.read(cache_key)).to eq(cached_value)
end
@@ -72,6 +77,9 @@ RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code
end
context 'when counts calculated from a branch that exists upstream' do
+ let_it_be(:source_repo) { create(:project, :repository, :public).repository }
+ let_it_be(:fork_repo) { fork_project(source_repo.project, user, { repository: true }).repository }
+
let(:fork_branch) { 'feature' }
it 'compares the fork branch to upstream default branch' do
@@ -94,5 +102,61 @@ RSpec.describe Projects::Forks::DivergenceCounts, feature_category: :source_code
expect_cached_counts([2, 30])
end
end
+
+ context 'when specified branch does not exist' do
+ it 'returns nils as counts' do
+ counts = described_class.new(fork_repo.project, 'non-existent-branch').counts
+ expect(counts).to eq({ ahead: nil, behind: nil })
+ end
+ end
+ end
+
+ describe '#update!', :use_clean_rails_redis_caching do
+ it 'updates the cache with the specified value' do
+ value = { source_sha: source_repo.commit.sha, sha: fork_repo.commit.sha, counts: [0, 0], has_conflicts: true }
+
+ described_class.new(fork_repo.project, fork_branch).update!(value)
+
+ expect(Rails.cache.read(cache_key)).to eq(value)
+ end
+ end
+
+ describe '#has_conflicts', :use_clean_rails_redis_caching do
+ it 'returns whether merge for the stored commits failed due to conflicts' do
+ details = described_class.new(fork_repo.project, fork_branch)
+
+ expect do
+ value = { source_sha: source_repo.commit.sha, sha: fork_repo.commit.sha, counts: [0, 0], has_conflicts: true }
+
+ details.update!(value)
+ end.to change { details.has_conflicts? }.from(false).to(true)
+ end
+ end
+
+ describe '#exclusive_lease' do
+ it 'returns exclusive lease to the details' do
+ key = ['project_details', fork_repo.project.id, fork_branch].join(':')
+ uuid = SecureRandom.uuid
+ details = described_class.new(fork_repo.project, fork_branch)
+
+ expect(Gitlab::ExclusiveLease).to receive(:get_uuid).with(key).and_return(uuid)
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(
+ key, uuid: uuid, timeout: described_class::LEASE_TIMEOUT
+ ).and_call_original
+
+ expect(details.exclusive_lease).to be_a(Gitlab::ExclusiveLease)
+ end
+ end
+
+ describe 'syncing?', :use_clean_rails_redis_caching do
+ it 'returns whether there is a sync in progress' do
+ details = described_class.new(fork_repo.project, fork_branch)
+
+ expect(details.exclusive_lease.try_obtain).to be_present
+ expect(details.syncing?).to eq(true)
+
+ details.exclusive_lease.cancel
+ expect(details.syncing?).to eq(false)
+ end
end
end
diff --git a/spec/models/projects/import_export/relation_export_spec.rb b/spec/models/projects/import_export/relation_export_spec.rb
index 8643fbc7b46..c724f30250d 100644
--- a/spec/models/projects/import_export/relation_export_spec.rb
+++ b/spec/models/projects/import_export/relation_export_spec.rb
@@ -2,9 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::RelationExport, type: :model do
- subject { create(:project_relation_export) }
-
+RSpec.describe Projects::ImportExport::RelationExport, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:project_export_job) }
it { is_expected.to have_one(:upload) }
@@ -13,12 +11,16 @@ RSpec.describe Projects::ImportExport::RelationExport, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:project_export_job) }
it { is_expected.to validate_presence_of(:relation) }
- it { is_expected.to validate_uniqueness_of(:relation).scoped_to(:project_export_job_id) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to validate_numericality_of(:status).only_integer }
it { is_expected.to validate_length_of(:relation).is_at_most(255) }
it { is_expected.to validate_length_of(:jid).is_at_most(255) }
it { is_expected.to validate_length_of(:export_error).is_at_most(300) }
+
+ it 'validates uniquness of the relation attribute' do
+ create(:project_relation_export)
+ expect(subject).to validate_uniqueness_of(:relation).scoped_to(:project_export_job_id)
+ end
end
describe '.by_relation' do
@@ -52,4 +54,16 @@ RSpec.describe Projects::ImportExport::RelationExport, type: :model do
expect(described_class.relation_names_list).not_to include('events', 'notes')
end
end
+
+ describe '#mark_as_failed' do
+ it 'sets status to failed and sets the export error', :aggregate_failures do
+ relation_export = create(:project_relation_export)
+
+ relation_export.mark_as_failed("Error message")
+ relation_export.reload
+
+ expect(relation_export.failed?).to eq(true)
+ expect(relation_export.export_error).to eq("Error message")
+ end
+ end
end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 71e22f848cc..c99c92e6c19 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranch do
+RSpec.describe ProtectedBranch, feature_category: :source_code_management do
subject { build_stubbed(:protected_branch) }
describe 'Associations' do
@@ -214,85 +214,52 @@ RSpec.describe ProtectedBranch do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:protected_branch) { create(:protected_branch, project: project, name: "“jawn”") }
- let(:rely_on_new_cache) { true }
-
- shared_examples_for 'hash based cache implementation' do
- it 'calls only hash based cache implementation' do
- expect_next_instance_of(ProtectedBranches::CacheService) do |instance|
- expect(instance).to receive(:fetch).with('missing-branch', anything).and_call_original
- end
-
- expect(Rails.cache).not_to receive(:fetch)
-
- described_class.protected?(project, 'missing-branch')
- end
- end
-
before do
- 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)
end
- context 'Dry-run: true (rely_on_protected_branches_cache is off, new hash-based is used)' do
- let(:rely_on_new_cache) { false }
+ 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
- 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
+ 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)).to eq(true)
- 2.times { described_class.protected?(project, protected_branch.name) }
- end
+ ProtectedBranches::UpdateService.new(project, project.owner, name: 'ber').execute(branch)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- it_behaves_like 'hash based cache implementation'
+ ProtectedBranches::DestroyService.new(project, project.owner).execute(branch)
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
- 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)).to eq(true)
+ context 'when project is updated' do
+ it 'does not invalidate a cache' do
+ expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
- ProtectedBranches::UpdateService.new(project, project.owner, name: 'ber').execute(branch)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
+ project.touch
- ProtectedBranches::DestroyService.new(project, project.owner).execute(branch)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
-
- it_behaves_like 'hash based cache implementation'
-
- context 'when project is updated' do
- it 'does not invalidate a cache' do
- expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
-
- project.touch
-
- described_class.protected?(project, protected_branch.name)
- end
+ described_class.protected?(project, protected_branch.name)
end
+ end
- context 'when other project protected branch is updated' do
- it 'does not invalidate the current project cache' do
- expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
+ context 'when other project protected branch is updated' do
+ it 'does not invalidate the current project cache' do
+ expect(described_class).not_to receive(:matching).with(protected_branch.name, protected_refs: anything)
- another_project = create(:project)
- ProtectedBranches::CreateService.new(another_project, another_project.owner, name: 'bar').execute
+ another_project = create(:project)
+ ProtectedBranches::CreateService.new(another_project, another_project.owner, name: 'bar').execute
- described_class.protected?(project, protected_branch.name)
- end
+ described_class.protected?(project, protected_branch.name)
end
+ end
- it 'correctly uses the cached version' do
- expect(described_class).not_to receive(:matching)
+ it 'correctly uses the cached version' do
+ expect(described_class).not_to receive(:matching)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
+ expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index b8780b3faae..f970e818db9 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -413,6 +413,27 @@ RSpec.describe Repository, feature_category: :source_code_management do
repository.commits('master', limit: 1)
end
end
+
+ context 'when include_referenced_by is passed' do
+ context 'when commit has references' do
+ let(:ref) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+ let(:include_referenced_by) { ['refs/tags'] }
+
+ subject { repository.commits(ref, limit: 1, include_referenced_by: include_referenced_by).first }
+
+ it 'returns commits with referenced_by excluding that match the patterns' do
+ expect(subject.referenced_by).to match_array(['refs/tags/v1.1.0'])
+ end
+
+ context 'when matching multiple references' do
+ let(:include_referenced_by) { ['refs/tags', 'refs/heads'] }
+
+ it 'returns commits with referenced_by that match the patterns' do
+ expect(subject.referenced_by).to match_array(['refs/tags/v1.1.0', 'refs/heads/improve/awesome', 'refs/heads/merge-test'])
+ end
+ end
+ end
+ end
end
context "when 'author' is set" do
@@ -682,6 +703,14 @@ RSpec.describe Repository, feature_category: :source_code_management do
it { is_expected.to be_nil }
end
+
+ context 'when root reference is empty' do
+ subject { empty_repo.merged_to_root_ref?('master') }
+
+ let(:empty_repo) { build(:project, :empty_repo).repository }
+
+ it { is_expected.to be_nil }
+ end
end
describe "#root_ref_sha" do
@@ -690,7 +719,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
subject { repository.root_ref_sha }
before do
- allow(repository).to receive(:commit).with(repository.root_ref) { commit }
+ allow(repository).to receive(:head_commit) { commit }
end
it { is_expected.to eq(commit.sha) }
@@ -1020,8 +1049,34 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
+ describe "#move_dir_files" do
+ it 'move directory files successfully' do
+ expect do
+ repository.move_dir_files(user, 'files/new_js', 'files/js',
+ branch_name: 'master',
+ message: 'move directory images to new_images',
+ author_email: author_email,
+ author_name: author_name)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
+ files = repository.ls_files('master')
+
+ expect(files).not_to include('files/js/application.js')
+ expect(files).to include('files/new_js/application.js')
+ end
+
+ it 'skips commit with same path' do
+ expect do
+ repository.move_dir_files(user, 'files/js', 'files/js',
+ branch_name: 'master',
+ message: 'no commit',
+ author_email: author_email,
+ author_name: author_name)
+ end.to change { repository.count_commits(ref: 'master') }.by(0)
+ end
+ end
+
describe "#delete_file" do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
it 'removes file successfully' do
expect do
@@ -1422,46 +1477,38 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
- [true, false].each do |ff|
- context "with feature flag license_from_gitaly=#{ff}" do
- before do
- stub_feature_flags(license_from_gitaly: ff)
- end
-
- describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
- let(:project) { create(:project, :repository) }
+ describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, :repository) }
- before do
- repository.delete_file(user, 'LICENSE',
- message: 'Remove LICENSE', branch_name: 'master')
- end
+ before do
+ repository.delete_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
+ end
- it 'returns nil when no license is detected' do
- expect(repository.license).to be_nil
- end
+ it 'returns nil when no license is detected' do
+ expect(repository.license).to be_nil
+ end
- it 'returns nil when the repository does not exist' do
- expect(repository).to receive(:exists?).and_return(false)
+ it 'returns nil when the repository does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
- expect(repository.license).to be_nil
- end
+ expect(repository.license).to be_nil
+ end
- it 'returns other when the content is not recognizable' do
- repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
- message: 'Add LICENSE', branch_name: 'master')
+ it 'returns other when the content is not recognizable' do
+ repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
+ message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license_key).to eq('other')
- end
+ expect(repository.license_key).to eq('other')
+ end
- it 'returns the license' do
- license = Licensee::License.new('mit')
- repository.create_file(user, 'LICENSE',
- license.content,
- message: 'Add LICENSE', branch_name: 'master')
+ it 'returns the license' do
+ license = Licensee::License.new('mit')
+ repository.create_file(user, 'LICENSE',
+ license.content,
+ message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license_key).to eq(license.key)
- end
- end
+ expect(repository.license_key).to eq(license.key)
end
end
@@ -2302,7 +2349,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
:contribution_guide,
:changelog,
:license_blob,
- :license_licensee,
:license_gitaly,
:gitignore,
:gitlab_ci_yml,
@@ -2957,11 +3003,10 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(readme_path license_blob license_licensee license_gitaly))
+ .with(%i(readme_path license_blob license_gitaly))
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
- expect(repository).to receive(:license_licensee)
expect(repository).to receive(:license_gitaly)
repository.refresh_method_caches(%i(readme license))
diff --git a/spec/models/serverless/domain_cluster_spec.rb b/spec/models/serverless/domain_cluster_spec.rb
deleted file mode 100644
index 487385c62c1..00000000000
--- a/spec/models/serverless/domain_cluster_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Serverless::DomainCluster do
- subject { create(:serverless_domain_cluster) }
-
- describe 'default values' do
- subject(:domain_cluster) { build(:serverless_domain_cluster) }
-
- before do
- allow(::Serverless::Domain).to receive(:generate_uuid).and_return('randomtoken')
- end
-
- it { expect(domain_cluster.uuid).to eq('randomtoken') }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:pages_domain) }
- it { is_expected.to validate_presence_of(:knative) }
-
- it { is_expected.to validate_presence_of(:uuid) }
- it { is_expected.to validate_length_of(:uuid).is_equal_to(::Serverless::Domain::UUID_LENGTH) }
- it { is_expected.to validate_uniqueness_of(:uuid) }
-
- it 'validates that uuid has only hex characters' do
- subject = build(:serverless_domain_cluster, uuid: 'z1234567890123')
- subject.valid?
-
- expect(subject.errors[:uuid]).to include('only allows hex characters')
- end
- end
-
- describe 'associations' do
- it { is_expected.to belong_to(:pages_domain) }
- it { is_expected.to belong_to(:knative) }
- it { is_expected.to belong_to(:creator).optional }
- end
-
- describe 'uuid' do
- context 'when nil' do
- it 'generates a value by default' do
- attributes = build(:serverless_domain_cluster).attributes.merge(uuid: nil)
- expect(::Serverless::Domain).to receive(:generate_uuid).and_call_original
-
- subject = Serverless::DomainCluster.new(attributes)
-
- expect(subject.uuid).not_to be_blank
- end
- end
-
- context 'when not nil' do
- it 'does not override the existing value' do
- uuid = 'abcd1234567890'
- expect(build(:serverless_domain_cluster, uuid: uuid).uuid).to eq(uuid)
- end
- end
- end
-
- describe 'cluster' do
- it { is_expected.to respond_to(:cluster) }
- end
-
- describe 'domain' do
- it { is_expected.to respond_to(:domain) }
- end
-
- describe 'certificate' do
- it { is_expected.to respond_to(:certificate) }
- end
-
- describe 'key' do
- it { is_expected.to respond_to(:key) }
- end
-end
diff --git a/spec/models/serverless/domain_spec.rb b/spec/models/serverless/domain_spec.rb
deleted file mode 100644
index f997b28b149..00000000000
--- a/spec/models/serverless/domain_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Serverless::Domain do
- let(:function_name) { 'test-function' }
- let(:pages_domain_name) { 'serverless.gitlab.io' }
- let(:pages_domain) { create(:pages_domain, :instance_serverless, domain: pages_domain_name) }
- let!(:serverless_domain_cluster) { create(:serverless_domain_cluster, uuid: 'abcdef12345678', pages_domain: pages_domain) }
- let(:valid_cluster_uuid) { 'aba1cdef123456f278' }
- let(:invalid_cluster_uuid) { 'aba1cdef123456f178' }
- let!(:environment) { create(:environment, name: 'test') }
-
- let(:valid_uri) { "https://#{function_name}-#{valid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
- let(:valid_fqdn) { "#{function_name}-#{valid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
- let(:invalid_uri) { "https://#{function_name}-#{invalid_cluster_uuid}#{"%x" % environment.id}-#{environment.slug}.#{pages_domain_name}" }
-
- shared_examples 'a valid Domain' do
- describe '#uri' do
- it 'matches valid URI' do
- expect(subject.uri.to_s).to eq valid_uri
- end
- end
-
- describe '#function_name' do
- it 'returns function_name' do
- expect(subject.function_name).to eq function_name
- end
- end
-
- describe '#serverless_domain_cluster' do
- it 'returns serverless_domain_cluster' do
- expect(subject.serverless_domain_cluster).to eq serverless_domain_cluster
- end
- end
-
- describe '#environment' do
- it 'returns environment' do
- expect(subject.environment).to eq environment
- end
- end
- end
-
- describe '.new' do
- context 'with valid arguments' do
- subject do
- described_class.new(
- function_name: function_name,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- it_behaves_like 'a valid Domain'
- end
-
- context 'with invalid arguments' do
- subject do
- described_class.new(
- function_name: function_name,
- environment: environment
- )
- end
-
- it { is_expected.not_to be_valid }
- end
-
- context 'with nil cluster argument' do
- subject do
- described_class.new(
- function_name: function_name,
- serverless_domain_cluster: nil,
- environment: environment
- )
- end
-
- it { is_expected.not_to be_valid }
- end
- end
-
- describe '.generate_uuid' do
- it 'has 14 characters' do
- expect(described_class.generate_uuid.length).to eq(described_class::UUID_LENGTH)
- end
-
- it 'consists of only hexadecimal characters' do
- expect(described_class.generate_uuid).to match(/\A\h+\z/)
- end
-
- it 'uses random characters' do
- uuid = 'abcd1234567890'
-
- expect(SecureRandom).to receive(:hex).with(described_class::UUID_LENGTH / 2).and_return(uuid)
- expect(described_class.generate_uuid).to eq(uuid)
- end
- end
-end
diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb
deleted file mode 100644
index 632f5eba5c3..00000000000
--- a/spec/models/serverless/function_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::Serverless::Function do
- let(:project) { create(:project) }
- let(:func) { described_class.new(project, 'test', 'test-ns') }
-
- it 'has a proper id' do
- expect(func.id).to eql("#{project.id}/test/test-ns")
- expect(func.name).to eql("test")
- expect(func.namespace).to eql("test-ns")
- end
-
- it 'can decode an identifier' do
- f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns")
-
- expect(f.name).to eql("testfunc")
- expect(f.namespace).to eql("dummy-ns")
- end
-end
diff --git a/spec/models/service_desk/custom_email_verification_spec.rb b/spec/models/service_desk/custom_email_verification_spec.rb
new file mode 100644
index 00000000000..f0a6028f21d
--- /dev/null
+++ b/spec/models/service_desk/custom_email_verification_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ServiceDesk::CustomEmailVerification, feature_category: :service_desk do
+ let(:user) { build_stubbed(:user) }
+ let(:project) { build_stubbed(:project) }
+ let(:verification) { build_stubbed(:service_desk_custom_email_verification, project: project) }
+ let(:token) { 'XXXXXXXXXXXX' }
+
+ describe '.generate_token' do
+ it 'matches expected output' do
+ expect(described_class.generate_token).to match(/\A\p{Alnum}{12}\z/)
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:state) }
+ end
+
+ describe '#accepted_until' do
+ context 'when no custom email is set up' do
+ it 'returns nil' do
+ expect(subject.accepted_until).to be_nil
+ end
+ end
+
+ context 'when custom email is set up' do
+ subject { verification.accepted_until }
+
+ it { is_expected.to be_nil }
+
+ context 'when verification process started' do
+ let(:triggered_at) { 2.minutes.ago }
+
+ before do
+ verification.assign_attributes(
+ state: "running",
+ triggered_at: triggered_at,
+ triggerer: user,
+ token: token
+ )
+ end
+
+ it { is_expected.to eq(described_class::TIMEFRAME.since(triggered_at)) }
+ end
+ end
+ end
+
+ describe '#in_timeframe?' do
+ context 'when no custom email is set up' do
+ it 'returns false' do
+ expect(subject).not_to be_in_timeframe
+ end
+ end
+
+ context 'when custom email is set up' do
+ it { is_expected.not_to be_in_timeframe }
+
+ context 'when verification process started' do
+ let(:triggered_at) { 1.second.ago }
+
+ before do
+ subject.assign_attributes(
+ state: "running",
+ triggered_at: triggered_at,
+ triggerer: user,
+ token: token
+ )
+ end
+
+ it { is_expected.to be_in_timeframe }
+
+ context 'and timeframe was missed' do
+ let(:triggered_at) { (described_class::TIMEFRAME + 1).ago }
+
+ before do
+ subject.triggered_at = triggered_at
+ end
+
+ it { is_expected.not_to be_in_timeframe }
+ end
+ end
+ end
+ end
+
+ describe 'encrypted #token' do
+ subject { build_stubbed(:service_desk_custom_email_verification, token: token) }
+
+ it 'saves and retrieves the encrypted token and iv correctly' do
+ expect(subject.encrypted_token).not_to be_nil
+ expect(subject.encrypted_token_iv).not_to be_nil
+
+ expect(subject.token).to eq(token)
+ end
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:triggerer) }
+
+ it 'can access service desk setting from project' do
+ setting = build_stubbed(:service_desk_setting, project: project)
+
+ expect(verification.service_desk_setting).to eq(setting)
+ end
+ end
+end
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index 32c36375a3d..b99494e6736 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
+ let(:verification) { build(:service_desk_custom_email_verification) }
+ let(:project) { build(:project) }
+
describe 'validations' do
subject(:service_desk_setting) { create(:service_desk_setting) }
@@ -23,6 +26,8 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
context 'when custom_email_enabled is true' do
before do
+ # Test without ServiceDesk::CustomEmailVerification for simplicity
+ # See dedicated simplified tests below
subject.custom_email_enabled = true
end
@@ -55,7 +60,18 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
it { is_expected.not_to allow_value('/example').for(:custom_email_smtp_address) }
end
- describe '.valid_issue_template' do
+ context 'when custom email verification is present/was triggered' do
+ before do
+ subject.project.service_desk_custom_email_verification = verification
+ end
+
+ it { is_expected.to validate_presence_of(:custom_email) }
+ it { is_expected.to validate_presence_of(:custom_email_smtp_username) }
+ it { is_expected.to validate_presence_of(:custom_email_smtp_port) }
+ it { is_expected.to validate_presence_of(:custom_email_smtp_address) }
+ end
+
+ describe '#valid_issue_template' do
let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) }
it 'is not valid if template does not exist' do
@@ -73,7 +89,20 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
end
end
- describe '.valid_project_key' do
+ describe '#custom_email_address_for_verification' do
+ it 'returns nil' do
+ expect(subject.custom_email_address_for_verification).to be_nil
+ end
+
+ context 'when custom_email exists' do
+ it 'returns correct verification address' do
+ subject.custom_email = 'support@example.com'
+ expect(subject.custom_email_address_for_verification).to eq('support+verify@example.com')
+ end
+ end
+ end
+
+ describe '#valid_project_key' do
# Creates two projects with same full path slug
# group1/test/one and group1/test-one will both have 'group-test-one' slug
let_it_be(:group) { create(:group) }
@@ -109,15 +138,15 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
end
end
- describe 'encrypted password' do
+ describe 'encrypted #custom_email_smtp_password' do
let_it_be(:settings) do
create(
:service_desk_setting,
custom_email_enabled: true,
- custom_email: 'supersupport@example.com',
+ custom_email: 'support@example.com',
custom_email_smtp_address: 'smtp.example.com',
custom_email_smtp_port: 587,
- custom_email_smtp_username: 'supersupport@example.com',
+ custom_email_smtp_username: 'support@example.com',
custom_email_smtp_password: 'supersecret'
)
end
@@ -131,6 +160,24 @@ RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
end
describe 'associations' do
+ let(:custom_email_settings) do
+ build_stubbed(
+ :service_desk_setting,
+ custom_email: 'support@example.com',
+ custom_email_smtp_address: 'smtp.example.com',
+ custom_email_smtp_port: 587,
+ custom_email_smtp_username: 'support@example.com',
+ custom_email_smtp_password: 'supersecret'
+ )
+ end
+
it { is_expected.to belong_to(:project) }
+
+ it 'can access custom email verification from project' do
+ project.service_desk_custom_email_verification = verification
+ custom_email_settings.project = project
+
+ expect(custom_email_settings.custom_email_verification).to eq(verification)
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e87667d9604..04f1bffce0a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -174,6 +174,11 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:revoked_user_achievements).class_name('Achievements::UserAchievement').with_foreign_key('revoked_by_user_id').inverse_of(:revoked_by_user) }
it { is_expected.to have_many(:achievements).through(:user_achievements).class_name('Achievements::Achievement').inverse_of(:users) }
it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') }
+ it { is_expected.to have_many(:audit_events).with_foreign_key(:author_id).inverse_of(:user) }
+
+ it do
+ is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee)
+ end
describe 'default values' do
let(:user) { described_class.new }
@@ -188,6 +193,7 @@ RSpec.describe User, feature_category: :user_profile do
it { expect(user.notified_of_own_activity).to be_falsey }
it { expect(user.preferred_language).to eq(Gitlab::CurrentSettings.default_preferred_language) }
it { expect(user.theme_id).to eq(described_class.gitlab_config.default_theme) }
+ it { expect(user.color_scheme_id).to eq(Gitlab::CurrentSettings.default_syntax_highlighting_theme) }
end
describe '#user_detail' do
@@ -474,6 +480,37 @@ RSpec.describe User, feature_category: :user_profile do
expect(user).to be_valid
end
end
+
+ context 'namespace_move_dir_allowed' do
+ context 'when the user is not a new record' do
+ before do
+ expect(user.new_record?).to eq(false)
+ end
+
+ it 'checks when username changes' do
+ expect(user).to receive(:namespace_move_dir_allowed)
+
+ user.username = 'newuser'
+ user.validate
+ end
+
+ it 'does not check if the username did not change' do
+ expect(user).not_to receive(:namespace_move_dir_allowed)
+ expect(user.username_changed?).to eq(false)
+
+ user.validate
+ end
+ end
+
+ it 'does not check if the user is a new record' do
+ user = User.new(username: 'newuser')
+
+ expect(user.new_record?).to eq(true)
+ expect(user).not_to receive(:namespace_move_dir_allowed)
+
+ user.validate
+ end
+ end
end
describe 'name' do
@@ -978,10 +1015,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- describe 'and U2F' do
- it_behaves_like "returns the right users", :two_factor_via_u2f
- end
-
describe 'and WebAuthn' do
it_behaves_like "returns the right users", :two_factor_via_webauthn
end
@@ -997,26 +1030,6 @@ RSpec.describe User, feature_category: :user_profile do
expect(users_without_two_factor).not_to include(user_with_2fa.id)
end
- describe 'and u2f' do
- it 'excludes users with 2fa enabled via U2F' do
- user_with_2fa = create(:user, :two_factor_via_u2f)
- user_without_2fa = create(:user)
- users_without_two_factor = described_class.without_two_factor.pluck(:id)
-
- expect(users_without_two_factor).to include(user_without_2fa.id)
- expect(users_without_two_factor).not_to include(user_with_2fa.id)
- end
-
- it 'excludes users with 2fa enabled via OTP and U2F' do
- user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
- user_without_2fa = create(:user)
- users_without_two_factor = described_class.without_two_factor.pluck(:id)
-
- expect(users_without_two_factor).to include(user_without_2fa.id)
- expect(users_without_two_factor).not_to include(user_with_2fa.id)
- end
- end
-
describe 'and webauthn' do
it 'excludes users with 2fa enabled via WebAuthn' do
user_with_2fa = create(:user, :two_factor_via_webauthn)
@@ -2174,6 +2187,24 @@ RSpec.describe User, feature_category: :user_profile do
end
end
+ context 'Duo Auth' do
+ context 'when enabled via GitLab settings' do
+ before do
+ allow(::Gitlab.config.duo_auth).to receive(:enabled).and_return(true)
+ end
+
+ it { expect(user.two_factor_otp_enabled?).to eq(true) }
+ end
+
+ context 'when disabled via GitLab settings' do
+ before do
+ allow(::Gitlab.config.duo_auth).to receive(:enabled).and_return(false)
+ end
+
+ it { expect(user.two_factor_otp_enabled?).to eq(false) }
+ end
+ end
+
context 'FortiTokenCloud' do
context 'when enabled via GitLab settings' do
before do
@@ -2207,54 +2238,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
- context 'two_factor_u2f_enabled?' do
- let_it_be(:user) { create(:user, :two_factor) }
-
- context 'when webauthn feature flag is enabled' do
- context 'user has no U2F registration' do
- it { expect(user.two_factor_u2f_enabled?).to eq(false) }
- end
-
- 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))
-
- expect(user.two_factor_u2f_enabled?).to eq(false)
- end
- end
- end
-
- context 'when webauthn feature flag is disabled' do
- before do
- stub_feature_flags(webauthn: false)
- end
-
- context 'user has no U2F registration' do
- it { expect(user.two_factor_u2f_enabled?).to eq(false) }
- end
-
- 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))
-
- expect(user.two_factor_u2f_enabled?).to eq(true)
- end
- end
- end
- end
-
describe 'needs_new_otp_secret?', :freeze_time do
let(:user) { create(:user) }
@@ -2396,14 +2379,6 @@ RSpec.describe User, feature_category: :user_profile do
end
it_behaves_like 'manageable groups examples'
-
- context 'when feature flag :linear_user_manageable_groups is disabled' do
- before do
- stub_feature_flags(linear_user_manageable_groups: false)
- end
-
- it_behaves_like 'manageable groups examples'
- end
end
end
end
@@ -7104,43 +7079,105 @@ RSpec.describe User, feature_category: :user_profile do
context 'when user is confirmed' do
let(:user) { create(:user) }
- it 'is falsey' do
- expect(user.confirmed?).to be_truthy
- expect(subject).to be_falsey
+ it 'is false' do
+ expect(user.confirmed?).to be(true)
+ expect(subject).to be(false)
end
end
context 'when user is not confirmed' do
let_it_be(:user) { build_stubbed(:user, :unconfirmed, confirmation_sent_at: Time.current) }
- it 'is truthy when soft_email_confirmation feature is disabled' do
- stub_feature_flags(soft_email_confirmation: false)
- expect(subject).to be_truthy
+ context 'when email confirmation setting is set to `off`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'off')
+ end
+
+ it { is_expected.to be(false) }
end
- context 'when soft_email_confirmation feature is enabled' do
+ context 'when email confirmation setting is set to `soft`' do
before do
- stub_feature_flags(soft_email_confirmation: true)
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
end
- it 'is falsey when confirmation period is valid' do
- expect(subject).to be_falsey
+ context 'when confirmation period is valid' do
+ it { is_expected.to be(false) }
end
- it 'is truthy when confirmation period is expired' do
- travel_to(User.allow_unconfirmed_access_for.from_now + 1.day) do
- expect(subject).to be_truthy
+ context 'when confirmation period is expired' do
+ before do
+ travel_to(User.allow_unconfirmed_access_for.from_now + 1.day)
end
+
+ it { is_expected.to be(true) }
end
context 'when user has no confirmation email sent' do
let(:user) { build(:user, :unconfirmed, confirmation_sent_at: nil) }
- it 'is truthy' do
- expect(subject).to be_truthy
- end
+ it { is_expected.to be(true) }
end
end
+
+ context 'when email confirmation setting is set to `hard`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+ end
+
+ it { is_expected.to be(true) }
+ end
+ end
+ end
+
+ describe '#confirmation_period_valid?' do
+ subject { user.send(:confirmation_period_valid?) }
+
+ let_it_be(:user) { create(:user) }
+
+ context 'when email confirmation setting is set to `off`' do
+ before do
+ stub_feature_flags(soft_email_confirmation: false)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when email confirmation setting is set to `soft`' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
+ end
+
+ context 'when within confirmation window' do
+ before do
+ user.update!(confirmation_sent_at: Date.today)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when outside confirmation window' do
+ before do
+ user.update!(confirmation_sent_at: Date.today - described_class.confirm_within - 7.days)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'when email confirmation setting is set to `hard`' do
+ before do
+ stub_feature_flags(soft_email_confirmation: false)
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ describe '#in_confirmation_period?' do
+ it 'is expected to be an alias' do
+ expect(user.method(:in_confirmation_period?).original_name).to eq(:confirmation_period_valid?)
+ end
end
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index 1b177934ace..c30e79f79ce 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe WikiDirectory do
let_it_be(:toplevel1) { build(:wiki_page, title: 'aaa-toplevel1') }
let_it_be(:toplevel2) { build(:wiki_page, title: 'zzz-toplevel2') }
let_it_be(:toplevel3) { build(:wiki_page, title: 'zzz-toplevel3') }
+ let_it_be(:home) { build(:wiki_page, title: 'home') }
+ let_it_be(:homechild) { build(:wiki_page, title: 'Home/homechild') }
let_it_be(:parent1) { build(:wiki_page, title: 'parent1') }
let_it_be(:parent2) { build(:wiki_page, title: 'parent2') }
let_it_be(:child1) { build(:wiki_page, title: 'parent1/child1') }
@@ -24,13 +26,18 @@ RSpec.describe WikiDirectory do
it 'returns a nested array of entries' do
entries = described_class.group_pages(
- [toplevel1, toplevel2, toplevel3,
+ [toplevel1, toplevel2, toplevel3, home, homechild,
parent1, parent2, child1, child2, child3,
subparent, grandchild1, grandchild2].sort_by(&:title)
)
expect(entries).to match(
[
+ a_kind_of(WikiDirectory).and(
+ having_attributes(
+ slug: 'Home', entries: [homechild]
+ )
+ ),
toplevel1,
a_kind_of(WikiDirectory).and(
having_attributes(
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 21da06a222f..efade74688a 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -588,6 +588,20 @@ RSpec.describe WikiPage do
expect(page.content).to eq new_content
end
+ context 'when page combine with directory' do
+ it 'moving the file and directory' do
+ wiki.create_page('testpage/testtitle', 'content')
+ wiki.create_page('testpage', 'content')
+
+ page = wiki.find_page('testpage')
+ page.update(title: 'testfolder/testpage')
+
+ page = wiki.find_page('testfolder/testpage/testtitle')
+
+ expect(page.slug).to eq 'testfolder/testpage/testtitle'
+ end
+ end
+
describe 'in subdir' do
it 'moves the page to the root folder if the title is preceded by /' do
page = create_wiki_page(container, title: 'foo/Existing Page')
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 6aacaa3c119..13f17e11276 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -163,6 +163,30 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe 'transform_quick_action_params' do
+ let(:work_item) { build(:work_item, :task) }
+
+ subject(:transformed_params) do
+ work_item.transform_quick_action_params({
+ title: 'bar',
+ assignee_ids: ['foo']
+ })
+ end
+
+ it 'correctly separates widget params from regular params' do
+ expect(transformed_params).to eq({
+ common: {
+ title: 'bar'
+ },
+ widgets: {
+ assignees_widget: {
+ assignee_ids: ['foo']
+ }
+ }
+ })
+ end
+ end
+
describe 'callbacks' do
describe 'record_create_action' do
it 'records the creation action after saving' do
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
index 08f8f4d9663..3a4670c996f 100644
--- a/spec/models/work_items/widget_definition_spec.rb
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
::WorkItems::Widgets::Assignees,
::WorkItems::Widgets::StartAndDueDate,
::WorkItems::Widgets::Milestone,
- ::WorkItems::Widgets::Notes
+ ::WorkItems::Widgets::Notes,
+ ::WorkItems::Widgets::Notifications
]
if Gitlab.ee?
diff --git a/spec/models/work_items/widgets/notifications_spec.rb b/spec/models/work_items/widgets/notifications_spec.rb
new file mode 100644
index 00000000000..2942c149660
--- /dev/null
+++ b/spec/models/work_items/widgets/notifications_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Notifications, feature_category: :team_planning do
+ let_it_be(:work_item) { create(:work_item) }
+
+ describe '.type' do
+ it { expect(described_class.type).to eq(:notifications) }
+ end
+
+ describe '#type' do
+ it { expect(described_class.new(work_item).type).to eq(:notifications) }
+ end
+
+ describe '#subscribed?' do
+ it { expect(described_class.new(work_item).subscribed?(work_item.author, work_item.project)).to eq(true) }
+ end
+end
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index 9aa50876b55..92ad37145c0 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe Ci::PipelineSchedulePolicy, :models, :clean_gitlab_redis_cache do
end
it 'includes abilities to take ownership' do
- expect(policy).to be_allowed :take_ownership_pipeline_schedule
+ expect(policy).to be_allowed :admin_pipeline_schedule
end
end
end
diff --git a/spec/policies/ci/runner_machine_policy_spec.rb b/spec/policies/ci/runner_machine_policy_spec.rb
new file mode 100644
index 00000000000..8b95f2d7526
--- /dev/null
+++ b/spec/policies/ci/runner_machine_policy_spec.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerMachinePolicy, feature_category: :runner_fleet do
+ let_it_be(:owner) { create(:user) }
+
+ describe 'ability :read_runner_machine' do
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+
+ let_it_be_with_reload(:group) { create(:group, name: 'top-level', path: 'top-level') }
+ let_it_be_with_reload(:subgroup) { create(:group, name: 'subgroup', path: 'subgroup', parent: group) }
+ let_it_be_with_reload(:project) { create(:project, group: subgroup) }
+
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, :with_runner_machine) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, :with_runner_machine, groups: [group]) }
+ let_it_be(:project_runner) { create(:ci_runner, :project, :with_runner_machine, projects: [project]) }
+
+ let(:runner_machine) { runner.runner_machines.first }
+
+ subject(:policy) { described_class.new(user, runner_machine) }
+
+ before_all do
+ group.add_guest(guest)
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ group.add_owner(owner)
+ end
+
+ shared_examples 'a policy allowing reading instance runner machine depending on runner sharing' do
+ context 'with instance runner' do
+ let(:runner) { instance_runner }
+
+ it { expect_allowed :read_runner_machine }
+
+ context 'with shared runners disabled on projects' do
+ before do
+ project.update!(shared_runners_enabled: false)
+ end
+
+ it { expect_allowed :read_runner_machine }
+ end
+
+ context 'with shared runners disabled for groups and projects' do
+ before do
+ group.update!(shared_runners_enabled: false)
+ project.update!(shared_runners_enabled: false)
+ end
+
+ it { expect_disallowed :read_runner_machine }
+ end
+ end
+ end
+
+ shared_examples 'a policy allowing reading group runner machine depending on runner sharing' do
+ context 'with group runner' do
+ let(:runner) { group_runner }
+
+ it { expect_allowed :read_runner_machine }
+
+ context 'with sharing of group runners disabled' do
+ before do
+ project.update!(group_runners_enabled: false)
+ end
+
+ it { expect_disallowed :read_runner_machine }
+ end
+ end
+ end
+
+ shared_examples 'does not allow reading runners machines on any scope' do
+ context 'with instance runner' do
+ let(:runner) { instance_runner }
+
+ it { expect_disallowed :read_runner_machine }
+
+ context 'with shared runners disabled for groups and projects' do
+ before do
+ group.update!(shared_runners_enabled: false)
+ project.update!(shared_runners_enabled: false)
+ end
+
+ it { expect_disallowed :read_runner_machine }
+ end
+ end
+
+ context 'with group runner' do
+ let(:runner) { group_runner }
+
+ it { expect_disallowed :read_runner_machine }
+
+ context 'with sharing of group runners disabled' do
+ before do
+ project.update!(group_runners_enabled: false)
+ end
+
+ it { expect_disallowed :read_runner_machine }
+ end
+ end
+
+ context 'with project runner' do
+ let(:runner) { project_runner }
+
+ it { expect_disallowed :read_runner_machine }
+ end
+ end
+
+ context 'without access' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'does not allow reading runners machines on any scope'
+ end
+
+ context 'with guest access' do
+ let(:user) { guest }
+
+ it_behaves_like 'does not allow reading runners machines on any scope'
+ end
+
+ context 'with developer access' do
+ let(:user) { developer }
+
+ it_behaves_like 'a policy allowing reading instance runner machine depending on runner sharing'
+
+ it_behaves_like 'a policy allowing reading group runner machine depending on runner sharing'
+
+ context 'with project runner' do
+ let(:runner) { project_runner }
+
+ it { expect_disallowed :read_runner_machine }
+ end
+ end
+
+ context 'with maintainer access' do
+ let(:user) { maintainer }
+
+ it_behaves_like 'a policy allowing reading instance runner machine depending on runner sharing'
+
+ it_behaves_like 'a policy allowing reading group runner machine depending on runner sharing'
+
+ context 'with project runner' do
+ let(:runner) { project_runner }
+
+ it { expect_allowed :read_runner_machine }
+ end
+ end
+
+ context 'with owner access' do
+ let(:user) { owner }
+
+ it_behaves_like 'a policy allowing reading instance runner machine depending on runner sharing'
+
+ context 'with group runner' do
+ let(:runner) { group_runner }
+
+ it { expect_allowed :read_runner_machine }
+
+ context 'with sharing of group runners disabled' do
+ before do
+ project.update!(group_runners_enabled: false)
+ end
+
+ it { expect_allowed :read_runner_machine }
+ end
+ end
+
+ context 'with project runner' do
+ let(:runner) { project_runner }
+
+ it { expect_allowed :read_runner_machine }
+ end
+ end
+ end
+end
diff --git a/spec/policies/design_management/design_policy_spec.rb b/spec/policies/design_management/design_policy_spec.rb
index c62e97dcdb9..1c0270a969e 100644
--- a/spec/policies/design_management/design_policy_spec.rb
+++ b/spec/policies/design_management/design_policy_spec.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
require "spec_helper"
-RSpec.describe DesignManagement::DesignPolicy do
+RSpec.describe DesignManagement::DesignPolicy, feature_category: :portfolio_management do
include DesignManagementTestHelpers
let(:guest_design_abilities) { %i[read_design] }
- let(:developer_design_abilities) { %i[create_design destroy_design move_design] }
+ let(:developer_design_abilities) { %i[create_design destroy_design move_design update_design] }
let(:design_abilities) { guest_design_abilities + developer_design_abilities }
let_it_be(:guest) { create(:user) }
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 0575ba3237b..3d6d95bb122 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
let_it_be(:admin_user) { create(:admin) }
let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:service_account) { create(:user, :service_account) }
let_it_be(:migration_bot) { create(:user, :migration_bot) }
let_it_be(:security_bot) { create(:user, :security_bot) }
let_it_be_with_reload(:current_user) { create(:user) }
@@ -219,6 +220,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_api) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -285,6 +292,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
context 'inactive user' do
before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
current_user.update!(confirmed_at: nil, confirmation_sent_at: 5.days.ago)
end
@@ -345,6 +353,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:receive_notifications) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_disallowed(:receive_notifications) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -399,6 +413,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
describe 'inactive user' do
before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
current_user.update!(confirmed_at: nil)
end
@@ -433,6 +448,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_git) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
context 'user blocked pending approval' do
before do
current_user.block_pending_approval
@@ -497,6 +518,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
describe 'inactive user' do
before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
current_user.update!(confirmed_at: nil)
end
@@ -517,6 +539,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:use_slash_commands) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:use_slash_commands) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -571,6 +599,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:log_in) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_disallowed(:log_in) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -593,57 +627,51 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
end
describe 'create_instance_runners' do
- context 'create_runner_workflow flag enabled' do
- before do
- stub_feature_flags(create_runner_workflow: true)
- end
-
- context 'admin' do
- let(:current_user) { admin_user }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:create_instance_runners) }
- end
+ context 'admin' do
+ let(:current_user) { admin_user }
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:create_instance_runners) }
- end
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:create_instance_runners) }
end
- context 'with project_bot' do
- let(:current_user) { project_bot }
-
+ context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:create_instance_runners) }
end
+ end
- context 'with migration_bot' do
- let(:current_user) { migration_bot }
+ context 'with project_bot' do
+ let(:current_user) { project_bot }
- it { is_expected.to be_disallowed(:create_instance_runners) }
- end
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
- context 'with security_bot' do
- let(:current_user) { security_bot }
+ context 'with migration_bot' do
+ let(:current_user) { migration_bot }
- it { is_expected.to be_disallowed(:create_instance_runners) }
- end
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
- context 'with regular user' do
- let(:current_user) { user }
+ context 'with security_bot' do
+ let(:current_user) { security_bot }
- it { is_expected.to be_disallowed(:create_instance_runners) }
- end
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
- context 'with anonymous' do
- let(:current_user) { nil }
+ context 'with regular user' do
+ let(:current_user) { user }
- it { is_expected.to be_disallowed(:create_instance_runners) }
- end
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
end
- context 'create_runner_workflow flag disabled' do
+ context 'create_runner_workflow_for_admin flag disabled' do
before do
- stub_feature_flags(create_runner_workflow: false)
+ stub_feature_flags(create_runner_workflow_for_admin: false)
end
context 'admin' do
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 451db9eaf9c..003ca2512dc 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization do
+RSpec.describe GroupPolicy, feature_category: :system_access do
include AdminModeHelper
include_context 'GroupPolicy context'
@@ -933,13 +933,14 @@ RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization
describe 'observability' do
using RSpec::Parameterized::TableSyntax
- let(:allowed) { be_allowed(:read_observability) }
- let(:disallowed) { be_disallowed(:read_observability) }
+ let(:allowed_admin) { be_allowed(:read_observability) && be_allowed(:admin_observability) }
+ let(:allowed_read) { be_allowed(:read_observability) && be_disallowed(:admin_observability) }
+ let(:disallowed) { be_disallowed(:read_observability) && be_disallowed(:admin_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)
+ true | ref(:allowed_admin) | ref(:allowed_admin) | ref(:allowed_admin) | ref(:allowed_read) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed) | ref(:disallowed)
end
# rubocop:enable Layout/LineLength
@@ -1296,9 +1297,9 @@ RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization
end
end
- context 'create_runner_workflow flag enabled' do
+ context 'create_runner_workflow_for_namespace flag enabled' do
before do
- stub_feature_flags(create_runner_workflow: true)
+ stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'admin' do
@@ -1379,11 +1380,13 @@ RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization
end
end
- context 'with create_runner_workflow flag disabled' do
+ context 'with create_runner_workflow_for_namespace flag disabled' do
before do
- stub_feature_flags(create_runner_workflow: false)
+ stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
end
+ let_it_be(:other_group) { create(:group) }
+
context 'admin' do
let(:current_user) { admin }
@@ -1568,4 +1571,22 @@ RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization
end
end
end
+
+ describe 'achievements' do
+ let(:current_user) { owner }
+
+ specify { is_expected.to be_allowed(:read_achievement) }
+ specify { is_expected.to be_allowed(:admin_achievement) }
+ specify { is_expected.to be_allowed(:award_achievement) }
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ specify { is_expected.to be_disallowed(:read_achievement) }
+ specify { is_expected.to be_disallowed(:admin_achievement) }
+ specify { is_expected.to be_disallowed(:award_achievement) }
+ end
+ end
end
diff --git a/spec/policies/metrics/dashboard/annotation_policy_spec.rb b/spec/policies/metrics/dashboard/annotation_policy_spec.rb
index 9ea9f843f2c..2d1ef0ee0cb 100644
--- a/spec/policies/metrics/dashboard/annotation_policy_spec.rb
+++ b/spec/policies/metrics/dashboard/annotation_policy_spec.rb
@@ -14,9 +14,7 @@ RSpec.describe Metrics::Dashboard::AnnotationPolicy, :models do
end
it { expect(policy).to be_disallowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :create_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :update_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :delete_metrics_dashboard_annotation }
+ it { expect(policy).to be_disallowed :admin_metrics_dashboard_annotation }
end
context 'when reporter' do
@@ -25,9 +23,7 @@ RSpec.describe Metrics::Dashboard::AnnotationPolicy, :models do
end
it { expect(policy).to be_allowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :create_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :update_metrics_dashboard_annotation }
- it { expect(policy).to be_disallowed :delete_metrics_dashboard_annotation }
+ it { expect(policy).to be_disallowed :admin_metrics_dashboard_annotation }
end
context 'when developer' do
@@ -36,9 +32,7 @@ RSpec.describe Metrics::Dashboard::AnnotationPolicy, :models do
end
it { expect(policy).to be_allowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :create_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :update_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :delete_metrics_dashboard_annotation }
+ it { expect(policy).to be_allowed :admin_metrics_dashboard_annotation }
end
context 'when maintainer' do
@@ -47,9 +41,7 @@ RSpec.describe Metrics::Dashboard::AnnotationPolicy, :models do
end
it { expect(policy).to be_allowed :read_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :create_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :update_metrics_dashboard_annotation }
- it { expect(policy).to be_allowed :delete_metrics_dashboard_annotation }
+ it { expect(policy).to be_allowed :admin_metrics_dashboard_annotation }
end
end
diff --git a/spec/policies/project_group_link_policy_spec.rb b/spec/policies/project_group_link_policy_spec.rb
index 7c8a4619e47..9461f33decb 100644
--- a/spec/policies/project_group_link_policy_spec.rb
+++ b/spec/policies/project_group_link_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectGroupLinkPolicy, feature_category: :authentication_and_authorization do
+RSpec.describe ProjectGroupLinkPolicy, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:group2) { create(:group, :private) }
diff --git a/spec/policies/project_hook_policy_spec.rb b/spec/policies/project_hook_policy_spec.rb
index cfa7b6ee4bf..a71940c319e 100644
--- a/spec/policies/project_hook_policy_spec.rb
+++ b/spec/policies/project_hook_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectHookPolicy do
+RSpec.describe ProjectHookPolicy, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let(:hook) { create(:project_hook) }
@@ -15,7 +15,7 @@ RSpec.describe ProjectHookPolicy do
end
it "cannot read and destroy web-hooks" do
- expect(policy).to be_disallowed(:read_web_hook, :destroy_web_hook)
+ expect(policy).to be_disallowed(:destroy_web_hook)
end
end
@@ -25,7 +25,7 @@ RSpec.describe ProjectHookPolicy do
end
it "can read and destroy web-hooks" do
- expect(policy).to be_allowed(:read_web_hook, :destroy_web_hook)
+ expect(policy).to be_allowed(:destroy_web_hook)
end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index b2fb310aca3..3759539677a 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorization do
+RSpec.describe ProjectPolicy, feature_category: :system_access do
include ExternalAuthorizationServiceHelpers
include AdminModeHelper
include_context 'ProjectPolicy context'
@@ -441,6 +441,36 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
+ context 'importing work items' do
+ %w(reporter developer maintainer owner).each do |role|
+ context "with #{role}" do
+ let(:current_user) { send(role) }
+
+ it { is_expected.to be_allowed(:import_work_items) }
+ end
+ end
+
+ %w(guest anonymous).each do |role|
+ context "with #{role}" do
+ let(:current_user) { send(role) }
+
+ it { is_expected.to be_disallowed(:import_work_items) }
+ end
+ end
+
+ context 'with an admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect_allowed(:import_work_items) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect_disallowed(:import_work_items) }
+ end
+ end
+ end
+
context 'reading usage quotas' do
%w(maintainer owner).each do |role|
context "with #{role}" do
@@ -1219,7 +1249,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
it { is_expected.to be_allowed(:create_package) }
it { is_expected.to be_allowed(:read_package) }
it { is_expected.to be_allowed(:read_project) }
- it { is_expected.to be_disallowed(:destroy_package) }
+ it { is_expected.to be_allowed(:destroy_package) }
it_behaves_like 'package access with repository disabled'
end
@@ -1400,6 +1430,28 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
+ context 'infrastructure aws feature' do
+ %w(guest reporter developer).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it 'disallows managing aws' do
+ expect_disallowed(:admin_project_aws)
+ end
+ end
+ end
+
+ %w(maintainer owner).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it 'allows managing aws' do
+ expect_allowed(:admin_project_aws)
+ end
+ end
+ end
+ end
+
describe 'design permissions' do
include DesignManagementTestHelpers
@@ -2275,6 +2327,12 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
describe 'infrastructure feature' do
using RSpec::Parameterized::TableSyntax
+ before do
+ # assuming the default setting terraform_state.enabled=true
+ # the terraform_state permissions should follow the same logic as the other features
+ stub_config(terraform_state: { enabled: true })
+ end
+
let(:guest_permissions) { [] }
let(:developer_permissions) do
@@ -2338,10 +2396,35 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
end
+
+ context 'when terraform state management is disabled' do
+ before do
+ stub_config(terraform_state: { enabled: false })
+ end
+
+ with_them do
+ let(:current_user) { user_subject(role) }
+ let(:project) { project_subject(project_visibility) }
+
+ let(:developer_permissions) do
+ [:read_terraform_state]
+ end
+
+ let(:maintainer_permissions) do
+ developer_permissions + [:admin_terraform_state]
+ end
+
+ it 'always disallows the terraform_state feature' do
+ project.project_feature.update!(infrastructure_access_level: access_level)
+
+ expect_disallowed(*permissions_abilities(role))
+ end
+ end
+ end
end
describe 'access_security_and_compliance' do
- context 'when the "Security & Compliance" is enabled' do
+ context 'when the "Security and Compliance" is enabled' do
before do
project.project_feature.update!(security_and_compliance_access_level: Featurable::PRIVATE)
end
@@ -2387,7 +2470,7 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
- context 'when the "Security & Compliance" is not enabled' do
+ context 'when the "Security and Compliance" is not enabled' do
before do
project.project_feature.update!(security_and_compliance_access_level: Featurable::DISABLED)
end
@@ -2740,9 +2823,9 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
describe 'create_project_runners' do
- context 'create_runner_workflow flag enabled' do
+ context 'create_runner_workflow_for_namespace flag enabled' do
before do
- stub_feature_flags(create_runner_workflow: true)
+ stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
end
context 'admin' do
@@ -2810,9 +2893,9 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
- context 'create_runner_workflow flag disabled' do
+ context 'create_runner_workflow_for_namespace flag disabled' do
before do
- stub_feature_flags(create_runner_workflow: false)
+ stub_feature_flags(create_runner_workflow_for_namespace: [group])
end
context 'admin' do
@@ -2981,6 +3064,26 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
+ describe 'add_catalog_resource' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:current_user) { public_send(role) }
+
+ where(:role, :allowed) do
+ :owner | true
+ :maintainer | false
+ :developer | false
+ :reporter | false
+ :guest | false
+ end
+
+ with_them do
+ it do
+ expect(subject.can?(:add_catalog_resource)).to be(allowed)
+ end
+ end
+ end
+
describe 'read_code' do
let(:current_user) { create(:user) }
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index f8cba8e9203..f10150b819a 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -60,9 +60,13 @@ RSpec.describe BlobPresenter do
describe '#pipeline_editor_path' do
context 'when blob is .gitlab-ci.yml' do
before do
- project.repository.create_file(user, '.gitlab-ci.yml', '',
- message: 'Add a ci file',
- branch_name: 'main')
+ project.repository.create_file(
+ user,
+ '.gitlab-ci.yml',
+ '',
+ message: 'Add a ci file',
+ branch_name: 'main'
+ )
end
let(:blob) { repository.blob_at('main', '.gitlab-ci.yml') }
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index dedfe6925c5..3f30127b07f 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -228,16 +228,20 @@ RSpec.describe Ci::BuildRunnerPresenter do
let(:pipeline) { build.pipeline }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}",
- "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
+ is_expected.to contain_exactly(
+ "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}",
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}"
+ )
end
context 'when ref is tag' do
let(:build) { create(:ci_build, :tag) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}",
- "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
+ is_expected.to contain_exactly(
+ "+refs/tags/#{build.ref}:refs/tags/#{build.ref}",
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}"
+ )
end
context 'when GIT_DEPTH is zero' do
@@ -246,9 +250,11 @@ RSpec.describe Ci::BuildRunnerPresenter do
end
it 'returns the correct refspecs' do
- is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*',
- "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
+ is_expected.to contain_exactly(
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*',
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}"
+ )
end
end
end
@@ -273,10 +279,11 @@ RSpec.describe Ci::BuildRunnerPresenter do
end
it 'returns the correct refspecs' do
- is_expected
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/heads/*:refs/remotes/origin/*',
- '+refs/tags/*:refs/tags/*')
+ is_expected.to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/heads/*:refs/remotes/origin/*',
+ '+refs/tags/*:refs/tags/*'
+ )
end
end
@@ -284,8 +291,10 @@ RSpec.describe Ci::BuildRunnerPresenter do
let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ is_expected.to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}"
+ )
end
end
end
@@ -301,9 +310,10 @@ RSpec.describe Ci::BuildRunnerPresenter do
end
it 'exposes the persistent pipeline ref' do
- is_expected
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ is_expected.to contain_exactly(
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}"
+ )
end
end
end
@@ -327,16 +337,14 @@ RSpec.describe Ci::BuildRunnerPresenter do
context 'when there is a file variable to expand' do
before_all do
- create(:ci_variable, project: project,
- key: 'regular_var',
- value: 'value 1')
- create(:ci_variable, project: project,
- key: 'file_var',
- value: 'value 2',
- variable_type: :file)
- create(:ci_variable, project: project,
- key: 'var_with_variables',
- value: 'value 3 and $regular_var and $file_var and $undefined_var')
+ create(:ci_variable, project: project, key: 'regular_var', value: 'value 1')
+ create(:ci_variable, project: project, key: 'file_var', value: 'value 2', variable_type: :file)
+ create(
+ :ci_variable,
+ project: project,
+ key: 'var_with_variables',
+ value: 'value 3 and $regular_var and $file_var and $undefined_var'
+ )
end
it 'returns variables with expanded' do
@@ -353,16 +361,14 @@ RSpec.describe Ci::BuildRunnerPresenter do
context 'when there is a raw variable to expand' do
before_all do
- create(:ci_variable, project: project,
- key: 'regular_var',
- value: 'value 1')
- create(:ci_variable, project: project,
- key: 'raw_var',
- value: 'value 2',
- raw: true)
- create(:ci_variable, project: project,
- key: 'var_with_variables',
- value: 'value 3 and $regular_var and $raw_var and $undefined_var')
+ create(:ci_variable, project: project, key: 'regular_var', value: 'value 1')
+ create(:ci_variable, project: project, key: 'raw_var', value: 'value 2', raw: true)
+ create(
+ :ci_variable,
+ project: project,
+ key: 'var_with_variables',
+ value: 'value 3 and $regular_var and $raw_var and $undefined_var'
+ )
end
it 'returns expanded variables without expanding raws' do
diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb
index eba393da2b7..5ac270a8df8 100644
--- a/spec/presenters/commit_presenter_spec.rb
+++ b/spec/presenters/commit_presenter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CommitPresenter do
+RSpec.describe CommitPresenter, feature_category: :source_code_management do
let(:commit) { project.commit }
let(:presenter) { described_class.new(commit, current_user: user) }
@@ -95,4 +95,15 @@ RSpec.describe CommitPresenter do
expect(presenter.signature_html).to eq(signature)
end
end
+
+ describe '#tags_for_display' do
+ subject { presenter.tags_for_display }
+
+ let(:stubbed_tags) { %w[refs/tags/v1.0 refs/tags/v1.1] }
+
+ it 'removes the refs prefix from tags' do
+ allow(commit).to receive(:referenced_by).and_return(stubbed_tags)
+ expect(subject).to eq(%w[v1.0 v1.1])
+ end
+ end
end
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
index 22a86d04a5a..1e8fda08bdb 100644
--- a/spec/presenters/issue_presenter_spec.rb
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -35,16 +35,6 @@ RSpec.describe IssuePresenter do
context 'when issue type is task' do
let(:presented_issue) { task }
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'returns a work item url for the task' do
- expect(presenter.web_url).to eq(project_work_items_url(project, work_items_path: presented_issue.id))
- end
- end
-
it 'returns a work item url using iid for the task' do
expect(presenter.web_url).to eq(
project_work_items_url(project, work_items_path: presented_issue.iid, iid_path: true)
@@ -75,16 +65,6 @@ RSpec.describe IssuePresenter do
context 'when issue type is task' do
let(:presented_issue) { task }
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'returns a work item path for the task' do
- expect(presenter.issue_path).to eq(project_work_items_path(project, work_items_path: presented_issue.id))
- end
- end
-
it 'returns a work item path using iid for the task' do
expect(presenter.issue_path).to eq(
project_work_items_path(project, work_items_path: presented_issue.iid, iid_path: true)
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 31aa4778d3c..6f40d3f5b48 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -125,9 +125,12 @@ RSpec.describe MergeRequestPresenter do
let_it_be(:issue_b) { create(:issue, project: project) }
let_it_be(:resource) do
- create(:merge_request,
- source_project: project, target_project: project,
- description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}")
+ create(
+ :merge_request,
+ source_project: project,
+ target_project: project,
+ description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}"
+ )
end
before_all do
diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb
index 71ec3ee2d67..8caa70c988e 100644
--- a/spec/presenters/packages/detail/package_presenter_spec.rb
+++ b/spec/presenters/packages/detail/package_presenter_spec.rb
@@ -89,8 +89,7 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
let_it_be(:package) { create(:npm_package, :with_build, project: project) }
let_it_be(:package_file_build_info) do
- create(:package_file_build_info, package_file: package.package_files.first,
- pipeline: package.pipelines.first)
+ create(:package_file_build_info, package_file: package.package_files.first, pipeline: package.pipelines.first)
end
it 'returns details with package_file pipeline' do
diff --git a/spec/rails_autoload.rb b/spec/rails_autoload.rb
new file mode 100644
index 00000000000..d3518acf8b2
--- /dev/null
+++ b/spec/rails_autoload.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# Mimics Rails autoloading with zeitwerk when used outside of Rails.
+# This is used in:
+# * fast_spec_helper
+# * scripts/setup-test-env
+
+require 'zeitwerk'
+require 'active_support/string_inquirer'
+
+module Rails
+ extend self
+
+ def root
+ Pathname.new(File.expand_path('..', __dir__))
+ end
+
+ def env
+ @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
+ end
+
+ def autoloaders
+ @autoloaders ||= [
+ Zeitwerk::Loader.new.tap do |loader|
+ loader.inflector = _autoloader_inflector
+ end
+ ]
+ end
+
+ private
+
+ def _autoloader_inflector
+ # Try Rails 7 first.
+ require 'rails/autoloaders/inflector'
+
+ Rails::Autoloaders::Inflector
+ rescue LoadError
+ # Fallback to Rails 6.
+ require 'active_support/dependencies'
+ require 'active_support/dependencies/zeitwerk_integration'
+
+ ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
+ end
+end
+
+require_relative '../lib/gitlab'
+require_relative '../config/initializers/0_inject_enterprise_edition_module'
+require_relative '../config/initializers_before_autoloader/000_inflections'
+require_relative '../config/initializers_before_autoloader/004_zeitwerk'
+
+Rails.autoloaders.each do |autoloader|
+ autoloader.push_dir('lib')
+ autoloader.push_dir('ee/lib') if Gitlab.ee?
+ autoloader.push_dir('jh/lib') if Gitlab.jh?
+ autoloader.setup
+end
diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb
new file mode 100644
index 00000000000..3d3bfcd3f60
--- /dev/null
+++ b/spec/requests/admin/abuse_reports_controller_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::AbuseReportsController, type: :request, feature_category: :insider_threat do
+ include AdminModeHelper
+
+ let_it_be(:admin) { create(:admin) }
+
+ before do
+ enable_admin_mode!(admin)
+ sign_in(admin)
+ end
+
+ describe 'GET #index' do
+ let!(:open_report) { create(:abuse_report) }
+ let!(:closed_report) { create(:abuse_report, :closed) }
+
+ it 'returns open reports by default' do
+ get admin_abuse_reports_path
+
+ expect(assigns(:abuse_reports).count).to eq 1
+ expect(assigns(:abuse_reports).first.open?).to eq true
+ end
+
+ it 'returns reports by specified status' do
+ get admin_abuse_reports_path, params: { status: 'closed' }
+
+ expect(assigns(:abuse_reports).count).to eq 1
+ expect(assigns(:abuse_reports).first.closed?).to eq true
+ end
+
+ context 'when abuse_reports_list flag is disabled' do
+ before do
+ stub_feature_flags(abuse_reports_list: false)
+ end
+
+ it 'returns all reports by default' do
+ get admin_abuse_reports_path
+
+ expect(assigns(:abuse_reports).count).to eq 2
+ end
+ end
+ end
+end
diff --git a/spec/requests/admin/applications_controller_spec.rb b/spec/requests/admin/applications_controller_spec.rb
index c83137ebbce..367697b1289 100644
--- a/spec/requests/admin/applications_controller_spec.rb
+++ b/spec/requests/admin/applications_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Admin::ApplicationsController, :enable_admin_mode,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
let_it_be(:admin) { create(:admin) }
let_it_be(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
let_it_be(:show_path) { admin_application_path(application) }
diff --git a/spec/requests/admin/broadcast_messages_controller_spec.rb b/spec/requests/admin/broadcast_messages_controller_spec.rb
index 69b84d6d795..0143c9ce030 100644
--- a/spec/requests/admin/broadcast_messages_controller_spec.rb
+++ b/spec/requests/admin/broadcast_messages_controller_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_c
let_it_be(:invalid_broadcast_message) { { broadcast_message: { message: '' } } }
let_it_be(:test_message) { 'you owe me a new acorn' }
+ let_it_be(:test_preview) { '<p>Hello, world!</p>' }
before do
sign_in(create(:admin))
@@ -23,11 +24,11 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_c
end
describe 'POST /preview' do
- it 'renders preview partial' do
+ it 'renders preview html' do
post preview_admin_broadcast_messages_path, params: { broadcast_message: { message: "Hello, world!" } }
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to render_template(:_preview)
+ expect(response.body).to eq(test_preview)
end
end
diff --git a/spec/requests/admin/impersonation_tokens_controller_spec.rb b/spec/requests/admin/impersonation_tokens_controller_spec.rb
index 15212db0e77..11fc5d94292 100644
--- a/spec/requests/admin/impersonation_tokens_controller_spec.rb
+++ b/spec/requests/admin/impersonation_tokens_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Admin::ImpersonationTokensController, :enable_admin_mode,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
diff --git a/spec/requests/admin/projects_controller_spec.rb b/spec/requests/admin/projects_controller_spec.rb
new file mode 100644
index 00000000000..5ff49a30ed8
--- /dev/null
+++ b/spec/requests/admin/projects_controller_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::ProjectsController, :enable_admin_mode, feature_category: :projects do
+ let_it_be(:project) { create(:project, :public, name: 'test', description: 'test') }
+ let_it_be(:admin) { create(:admin) }
+
+ describe 'PUT #update' do
+ let(:project_params) { {} }
+ let(:params) { { project: project_params } }
+ let(:path_params) { { namespace_id: project.namespace.to_param, id: project.to_param } }
+
+ before do
+ sign_in(admin)
+ end
+
+ subject do
+ put admin_namespace_project_path(path_params), params: params
+ end
+
+ context 'when changing the name' do
+ let(:project_params) { { name: 'new name' } }
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'changes the name' do
+ expect { subject }.to change { project.reload.name }.to('new name')
+ end
+ end
+
+ context 'when changing the description' do
+ let(:project_params) { { description: 'new description' } }
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'changes the project description' do
+ expect { subject }.to change { project.reload.description }.to('new description')
+ end
+ end
+
+ context 'when changing the name to an invalid name' do
+ let(:project_params) { { name: 'invalid/project/name' } }
+
+ it 'does not change the name' do
+ expect { subject }.not_to change { project.reload.name }
+ end
+ end
+ end
+end
diff --git a/spec/requests/admin/version_check_controller_spec.rb b/spec/requests/admin/version_check_controller_spec.rb
index 47221bf37e5..a998c2f426b 100644
--- a/spec/requests/admin/version_check_controller_spec.rb
+++ b/spec/requests/admin/version_check_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::VersionCheckController, :enable_admin_mode, feature_category: :not_owned do
+RSpec.describe Admin::VersionCheckController, :enable_admin_mode, feature_category: :shared do
let(:admin) { create(:admin) }
before do
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 8c14ead9e42..45d1594c734 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AccessRequests, feature_category: :authentication_and_authorization do
+RSpec.describe API::AccessRequests, feature_category: :system_access do
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:access_requester) { create(:user) }
diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb
index d946ac17f3f..e88fba3fbe7 100644
--- a/spec/requests/api/admin/batched_background_migrations_spec.rb
+++ b/spec/requests/api/admin/batched_background_migrations_spec.rb
@@ -4,22 +4,23 @@ require 'spec_helper'
RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :database do
let(:admin) { create(:admin) }
- let(:unauthorized_user) { create(:user) }
describe 'GET /admin/batched_background_migrations/:id' do
let!(:migration) { create(:batched_background_migration, :paused) }
let(:database) { :main }
let(:params) { { database: database } }
+ let(:path) { "/admin/batched_background_migrations/#{migration.id}" }
+
+ it_behaves_like "GET request permissions for admin mode"
subject(:show_migration) do
- get api("/admin/batched_background_migrations/#{migration.id}", admin), params: { database: database }
+ get api(path, admin, admin_mode: true), params: { database: database }
end
it 'fetches the batched background migration' do
show_migration
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('paused')
expect(json_response['job_class_name']).to eq(migration.job_class_name)
@@ -29,7 +30,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the batched background migration does not exist' do
it 'returns 404' do
- get api("/admin/batched_background_migrations/#{non_existing_record_id}", admin), params: params
+ get api("/admin/batched_background_migrations/#{non_existing_record_id}", admin, admin_mode: true),
+ params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -50,19 +52,11 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
end
end
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- get api("/admin/batched_background_migrations/#{migration.id}", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- get api("/admin/batched_background_migrations/#{migration.id}", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
@@ -72,13 +66,15 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
describe 'GET /admin/batched_background_migrations' do
let!(:migration) { create(:batched_background_migration) }
+ let(:path) { '/admin/batched_background_migrations' }
+
+ it_behaves_like "GET request permissions for admin mode"
context 'when is an admin user' do
it 'returns batched background migrations' do
- get api('/admin/batched_background_migrations', admin)
+ get api(path, admin, admin_mode: true)
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(migration.id)
expect(json_response.first['job_class_name']).to eq(migration.job_class_name)
@@ -105,14 +101,14 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
- get api('/admin/batched_background_migrations', admin), params: params
+ get api(path, admin, admin_mode: true), params: params
end
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- get api("/admin/batched_background_migrations", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
@@ -127,10 +123,9 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
create(:batched_background_migration, :active, gitlab_schema: schema)
end
- get api('/admin/batched_background_migrations', admin), params: params
+ get api(path, admin, admin_mode: true), params: params
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(ci_database_migration.id)
expect(json_response.first['job_class_name']).to eq(ci_database_migration.job_class_name)
@@ -142,30 +137,24 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
end
end
end
-
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- get api('/admin/batched_background_migrations', unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'PUT /admin/batched_background_migrations/:id/resume' do
let!(:migration) { create(:batched_background_migration, :paused) }
let(:database) { :main }
let(:params) { { database: database } }
+ let(:path) { "/admin/batched_background_migrations/#{migration.id}/resume" }
+
+ it_behaves_like "PUT request permissions for admin mode"
subject(:resume) do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
end
it 'pauses the batched background migration' do
resume
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('active')
end
@@ -173,7 +162,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the batched background migration does not exist' do
it 'returns 404' do
- put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin, admin_mode: true),
+ params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -183,7 +173,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
let!(:migration) { create(:batched_background_migration, :failed) }
it 'returns 422' do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -206,34 +196,28 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
end
end
end
-
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'PUT /admin/batched_background_migrations/:id/pause' do
let!(:migration) { create(:batched_background_migration, :active) }
let(:database) { :main }
let(:params) { { database: database } }
+ let(:path) { "/admin/batched_background_migrations/#{migration.id}/pause" }
+
+ it_behaves_like "PUT request permissions for admin mode"
it 'pauses the batched background migration' do
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('paused')
end
@@ -241,7 +225,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the batched background migration does not exist' do
it 'returns 404' do
- put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin, admin_mode: true),
+ params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -251,7 +236,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
let!(:migration) { create(:batched_background_migration, :failed) }
it 'returns 422' do
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -268,27 +253,19 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
it 'uses the correct connection' do
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
end
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
end
end
end
-
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
end
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb
index 4bdc44cb583..dd4171b257a 100644
--- a/spec/requests/api/admin/ci/variables_spec.rb
+++ b/spec/requests/api/admin/ci/variables_spec.rb
@@ -5,68 +5,60 @@ require 'spec_helper'
RSpec.describe ::API::Admin::Ci::Variables do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
+ let_it_be(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { '/admin/ci/variables' }
describe 'GET /admin/ci/variables' do
- let!(:variable) { create(:ci_instance_variable) }
+ it_behaves_like 'GET request permissions for admin mode'
it 'returns instance-level variables for admins', :aggregate_failures do
- get api('/admin/ci/variables', admin)
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
end
- it 'does not return instance-level variables for regular users' do
- get api('/admin/ci/variables', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
it 'does not return instance-level variables for unauthorized users' do
- get api('/admin/ci/variables')
+ get api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
describe 'GET /admin/ci/variables/:key' do
- let!(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
it 'returns instance-level variable details for admins', :aggregate_failures do
- get api("/admin/ci/variables/#{variable.key}", admin)
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
expect(json_response['variable_type']).to eq(variable.variable_type)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
- get api('/admin/ci/variables/non_existing_variable', admin)
+ get api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'does not return instance-level variable details for regular users' do
- get api("/admin/ci/variables/#{variable.key}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
it 'does not return instance-level variable details for unauthorized users' do
- get api("/admin/ci/variables/#{variable.key}")
+ get api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
describe 'POST /admin/ci/variables' do
- context 'authorized user with proper permissions' do
- let!(:variable) { create(:ci_instance_variable) }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { key: 'KEY', value: 'VALUE' } }
+ end
+ context 'authorized user with proper permissions' do
it 'creates variable for admins', :aggregate_failures do
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: {
key: 'TEST_VARIABLE_2',
value: 'PROTECTED_VALUE_2',
@@ -76,7 +68,6 @@ RSpec.describe ::API::Admin::Ci::Variables do
}
end.to change { ::Ci::InstanceVariable.count }.by(1)
- expect(response).to have_gitlab_http_status(:created)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
@@ -90,13 +81,13 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
- post api("/admin/ci/variables", user),
+ post api(path, user, admin_mode: true),
params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
end
it 'creates variable with optional attributes', :aggregate_failures do
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: {
variable_type: 'file',
key: 'TEST_VARIABLE_2',
@@ -104,7 +95,6 @@ RSpec.describe ::API::Admin::Ci::Variables do
}
end.to change { ::Ci::InstanceVariable.count }.by(1)
- expect(response).to have_gitlab_http_status(:created)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
@@ -115,20 +105,20 @@ RSpec.describe ::API::Admin::Ci::Variables do
it 'does not allow to duplicate variable key' do
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: { key: variable.key, value: 'VALUE_2' }
end.not_to change { ::Ci::InstanceVariable.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'does not allow values above 10,000 characters' do
+ it 'does not allow values above 10,000 characters', :aggregate_failures do
too_long_message = <<~MESSAGE.strip
The value of the provided variable exceeds the 10000 character limit
MESSAGE
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: { key: 'too_long', value: SecureRandom.hex(10_001) }
end.not_to change { ::Ci::InstanceVariable.count }
@@ -138,17 +128,9 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
end
- context 'authorized user with invalid permissions' do
- it 'does not create variable' do
- post api('/admin/ci/variables', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'unauthorized user' do
it 'does not create variable' do
- post api('/admin/ci/variables')
+ post api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -156,20 +138,23 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
describe 'PUT /admin/ci/variables/:key' do
- let!(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+ let_it_be(:params) do
+ {
+ variable_type: 'file',
+ value: 'VALUE_1_UP',
+ protected: true,
+ masked: true,
+ raw: true
+ }
+ end
+
+ it_behaves_like 'PUT request permissions for admin mode'
context 'authorized user with proper permissions' do
it 'updates variable data', :aggregate_failures do
- put api("/admin/ci/variables/#{variable.key}", admin),
- params: {
- variable_type: 'file',
- value: 'VALUE_1_UP',
- protected: true,
- masked: true,
- raw: true
- }
-
- expect(response).to have_gitlab_http_status(:ok)
+ put api(path, admin, admin_mode: true), params: params
+
expect(variable.reload.value).to eq('VALUE_1_UP')
expect(variable.reload).to be_protected
expect(json_response['variable_type']).to eq('file')
@@ -182,28 +167,20 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
- put api("/admin/ci/variables/#{variable.key}", admin),
+ put api(path, admin, admin_mode: true),
params: { value: 'SENSITIVE', protected: true, masked: true }
end
it 'responds with 404 Not Found if requesting non-existing variable' do
- put api('/admin/ci/variables/non_existing_variable', admin)
+ put api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
- it 'does not update variable' do
- put api("/admin/ci/variables/#{variable.key}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'unauthorized user' do
it 'does not update variable' do
- put api("/admin/ci/variables/#{variable.key}")
+ put api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -211,35 +188,27 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
describe 'DELETE /admin/ci/variables/:key' do
- let!(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
context 'authorized user with proper permissions' do
it 'deletes variable' do
expect do
- delete api("/admin/ci/variables/#{variable.key}", admin)
-
- expect(response).to have_gitlab_http_status(:no_content)
+ delete api(path, admin, admin_mode: true)
end.to change { ::Ci::InstanceVariable.count }.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
- delete api('/admin/ci/variables/non_existing_variable', admin)
+ delete api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
- it 'does not delete variable' do
- delete api("/admin/ci/variables/#{variable.key}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'unauthorized user' do
it 'does not delete variable' do
- delete api("/admin/ci/variables/#{variable.key}")
+ delete api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index 7b510f74fd4..0a72f404e89 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_management do
include KubernetesHelpers
- let_it_be(:regular_user) { create(:user) }
let_it_be(:admin_user) { create(:admin) }
let_it_be(:project) { create(:project) }
let_it_be(:project_cluster) do
@@ -17,35 +16,27 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:project_cluster_id) { project_cluster.id }
describe "GET /admin/clusters" do
+ let_it_be(:path) { "/admin/clusters" }
let_it_be(:clusters) do
create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment)
end
- include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { get api("/admin/clusters", admin_user) }
- end
+ it_behaves_like 'GET request permissions for admin mode'
- context "when authenticated as a non-admin user" do
- it 'returns 403' do
- get api('/admin/clusters', regular_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ include_examples ':certificate_based_clusters feature flag API responses' do
+ let(:subject) { get api(path, admin_user, admin_mode: true) }
end
context "when authenticated as admin" do
before do
- get api("/admin/clusters", admin_user)
- end
-
- it 'returns 200' do
- expect(response).to have_gitlab_http_status(:ok)
+ get api(path, admin_user, admin_mode: true)
end
it 'includes pagination headers' do
expect(response).to include_pagination_headers
end
- it 'only returns the instance clusters' do
+ it 'only returns the instance clusters', :aggregate_failures do
cluster_ids = json_response.map { |cluster| cluster['id'] }
expect(cluster_ids).to match_array(clusters.pluck(:id))
expect(cluster_ids).not_to include(project_cluster_id)
@@ -60,19 +51,23 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let_it_be(:cluster) do
create(:cluster, :instance, :provided_by_gcp, :with_domain,
- platform_kubernetes: platform_kubernetes,
- user: admin_user)
+ { platform_kubernetes: platform_kubernetes,
+ user: admin_user })
end
let(:cluster_id) { cluster.id }
+ let(:path) { "/admin/clusters/#{cluster_id}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { get api("/admin/clusters/#{cluster_id}", admin_user) }
+ let(:subject) { get api(path, admin_user, admin_mode: true) }
end
context "when authenticated as admin" do
before do
- get api("/admin/clusters/#{cluster_id}", admin_user)
+ get api(path, admin_user, admin_mode: true)
end
context "when no cluster associated to the ID" do
@@ -84,15 +79,11 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
end
context "when cluster with cluster_id exists" do
- it 'returns 200' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
it 'returns the cluster with cluster_id' do
expect(json_response['id']).to eq(cluster.id)
end
- it 'returns the cluster information' do
+ it 'returns the cluster information', :aggregate_failures do
expect(json_response['provider_type']).to eq('gcp')
expect(json_response['platform_type']).to eq('kubernetes')
expect(json_response['environment_scope']).to eq('*')
@@ -102,21 +93,21 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
expect(json_response['managed']).to be_truthy
end
- it 'returns kubernetes platform information' do
+ it 'returns kubernetes platform information', :aggregate_failures do
platform = json_response['platform_kubernetes']
expect(platform['api_url']).to eq('https://kubernetes.example.com')
expect(platform['ca_cert']).to be_present
end
- it 'returns user information' do
+ it 'returns user information', :aggregate_failures do
user = json_response['user']
expect(user['id']).to eq(admin_user.id)
expect(user['username']).to eq(admin_user.username)
end
- it 'returns GCP provider information' do
+ it 'returns GCP provider information', :aggregate_failures do
gcp_provider = json_response['provider_gcp']
expect(gcp_provider['cluster_id']).to eq(cluster.id)
@@ -140,18 +131,11 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
context 'when trying to get a project cluster via the instance cluster endpoint' do
it 'returns 404' do
- get api("/admin/clusters/#{project_cluster_id}", admin_user)
+ get api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
-
- context "when authenticated as a non-admin user" do
- it 'returns 403' do
- get api("/admin/clusters/#{cluster_id}", regular_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
end
@@ -159,6 +143,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:api_url) { 'https://example.com' }
let(:authorization_type) { 'rbac' }
let(:clusterable) { Clusters::Instance.new }
+ let_it_be(:path) { '/admin/clusters/add' }
let(:platform_kubernetes_attributes) do
{
@@ -196,20 +181,20 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
}
end
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { cluster_params }
+ end
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { post api('/admin/clusters/add', admin_user), params: cluster_params }
+ let(:subject) { post api(path, admin_user, admin_mode: true), params: cluster_params }
end
context 'authorized user' do
before do
- post api('/admin/clusters/add', admin_user), params: cluster_params
+ post api(path, admin_user, admin_mode: true), params: cluster_params
end
context 'with valid params' do
- it 'responds with 201' do
- expect(response).to have_gitlab_http_status(:created)
- end
-
it 'creates a new Clusters::Cluster', :aggregate_failures do
cluster_result = Clusters::Cluster.find(json_response["id"])
platform_kubernetes = cluster_result.platform
@@ -271,7 +256,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
context 'when an instance cluster already exists' do
it 'allows user to add multiple clusters' do
- post api('/admin/clusters/add', admin_user), params: multiple_cluster_params
+ post api(path, admin_user, admin_mode: true), params: multiple_cluster_params
expect(Clusters::Instance.new.clusters.count).to eq(2)
end
@@ -280,8 +265,8 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
context 'with invalid params' do
context 'when missing a required parameter' do
- it 'responds with 400' do
- post api('/admin/clusters/add', admin_user), params: invalid_cluster_params
+ it 'responds with 400', :aggregate_failures do
+ post api(path, admin_user, admin_mode: true), params: invalid_cluster_params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('name is missing')
end
@@ -300,14 +285,6 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
end
end
end
-
- context 'non-authorized user' do
- it 'responds with 403' do
- post api('/admin/clusters/add', regular_user), params: cluster_params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'PUT /admin/clusters/:cluster_id' do
@@ -329,23 +306,25 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com')
end
+ let(:path) { "/admin/clusters/#{cluster.id}" }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { update_params }
+ end
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params }
+ let(:subject) { put api(path, admin_user, admin_mode: true), params: update_params }
end
context 'authorized user' do
before do
- put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params
+ put api(path, admin_user, admin_mode: true), params: update_params
cluster.reload
end
context 'with valid params' do
- it 'responds with 200' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'updates cluster attributes' do
+ it 'updates cluster attributes', :aggregate_failures do
expect(cluster.domain).to eq('new-domain.com')
expect(cluster.managed).to be_falsy
expect(cluster.enabled).to be_falsy
@@ -359,7 +338,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'does not update cluster attributes' do
+ it 'does not update cluster attributes', :aggregate_failures do
expect(cluster.domain).to eq('old-domain.com')
expect(cluster.managed).to be_truthy
expect(cluster.enabled).to be_truthy
@@ -422,7 +401,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
expect(response).to have_gitlab_http_status(:ok)
end
- it 'updates platform kubernetes attributes' do
+ it 'updates platform kubernetes attributes', :aggregate_failures do
platform_kubernetes = cluster.platform_kubernetes
expect(cluster.name).to eq('new-name')
@@ -435,26 +414,18 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:cluster_id) { 1337 }
it 'returns 404' do
- put api("/admin/clusters/#{cluster_id}", admin_user), params: update_params
+ put api("/admin/clusters/#{cluster_id}", admin_user, admin_mode: true), params: update_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when trying to update a project cluster via the instance cluster endpoint' do
it 'returns 404' do
- put api("/admin/clusters/#{project_cluster_id}", admin_user), params: update_params
+ put api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true), params: update_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
-
- context 'non-authorized user' do
- it 'responds with 403' do
- put api("/admin/clusters/#{cluster.id}", regular_user), params: update_params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'DELETE /admin/clusters/:cluster_id' do
@@ -464,17 +435,17 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
create(:cluster, :instance, :provided_by_gcp)
end
+ let_it_be(:path) { "/admin/clusters/#{cluster.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params }
+ let(:subject) { delete api(path, admin_user, admin_mode: true), params: cluster_params }
end
context 'authorized user' do
before do
- delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params
- end
-
- it 'responds with 204' do
- expect(response).to have_gitlab_http_status(:no_content)
+ delete api(path, admin_user, admin_mode: true), params: cluster_params
end
it 'deletes the cluster' do
@@ -485,25 +456,17 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:cluster_id) { 1337 }
it 'returns 404' do
- delete api("/admin/clusters/#{cluster_id}", admin_user)
+ delete api(path, admin_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when trying to update a project cluster via the instance cluster endpoint' do
it 'returns 404' do
- delete api("/admin/clusters/#{project_cluster_id}", admin_user)
+ delete api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
-
- context 'non-authorized user' do
- it 'responds with 403' do
- delete api("/admin/clusters/#{cluster.id}", regular_user), params: cluster_params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
end
diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb
index 2de7a66d803..dffe062c031 100644
--- a/spec/requests/api/admin/plan_limits_spec.rb
+++ b/spec/requests/api/admin/plan_limits_spec.rb
@@ -2,26 +2,19 @@
require 'spec_helper'
-RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owned do
- let_it_be(:user) { create(:user) }
+RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared do
let_it_be(:admin) { create(:admin) }
let_it_be(:plan) { create(:plan, name: 'default') }
+ let_it_be(:path) { '/application/plan_limits' }
describe 'GET /application/plan_limits' do
- context 'as a non-admin user' do
- it 'returns 403' do
- get api('/application/plan_limits', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ it_behaves_like 'GET request permissions for admin mode'
context 'as an admin user' do
context 'no params' do
- it 'returns plan limits' do
- get api('/application/plan_limits', admin)
+ it 'returns plan limits', :aggregate_failures do
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['ci_pipeline_size']).to eq(Plan.default.actual_limits.ci_pipeline_size)
expect(json_response['ci_active_jobs']).to eq(Plan.default.actual_limits.ci_active_jobs)
@@ -49,8 +42,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
@params = { plan_name: 'default' }
end
- it 'returns plan limits' do
- get api('/application/plan_limits', admin), params: @params
+ it 'returns plan limits', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: @params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
@@ -80,8 +73,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
@params = { plan_name: 'my-plan' }
end
- it 'returns validation error' do
- get api('/application/plan_limits', admin), params: @params
+ it 'returns validation error', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: @params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('plan_name does not have a valid value')
@@ -91,18 +84,14 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
describe 'PUT /application/plan_limits' do
- context 'as a non-admin user' do
- it 'returns 403' do
- put api('/application/plan_limits', user), params: { plan_name: 'default' }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { 'plan_name': 'default' } }
end
context 'as an admin user' do
context 'correct params' do
- it 'updates multiple plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'updates multiple plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'ci_pipeline_size': 101,
'ci_active_jobs': 102,
@@ -124,7 +113,6 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
'pipeline_hierarchy_size': 250
}
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['ci_pipeline_size']).to eq(101)
expect(json_response['ci_active_jobs']).to eq(102)
@@ -146,8 +134,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
expect(json_response['pipeline_hierarchy_size']).to eq(250)
end
- it 'updates single plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'updates single plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'maven_max_file_size': 100
}
@@ -159,8 +147,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
context 'empty params' do
- it 'fails to update plan limits' do
- put api('/application/plan_limits', admin), params: {}
+ it 'fails to update plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to match('plan_name is missing')
@@ -168,8 +156,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
context 'params with wrong type' do
- it 'fails to update plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'fails to update plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'ci_pipeline_size': 'z',
'ci_active_jobs': 'y',
@@ -216,8 +204,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
context 'missing plan_name in params' do
- it 'fails to update plan limits' do
- put api('/application/plan_limits', admin), params: { 'conan_max_file_size': 0 }
+ it 'fails to update plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: { 'conan_max_file_size': 0 }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to match('plan_name is missing')
@@ -229,8 +217,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
Plan.default.actual_limits.update!({ 'golang_max_file_size': 1000 })
end
- it 'updates only declared plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'updates only declared plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'pypi_max_file_size': 200,
'golang_max_file_size': 999
diff --git a/spec/requests/api/admin/sidekiq_spec.rb b/spec/requests/api/admin/sidekiq_spec.rb
index 0b456721d4f..8bcd7884fd2 100644
--- a/spec/requests/api/admin/sidekiq_spec.rb
+++ b/spec/requests/api/admin/sidekiq_spec.rb
@@ -2,18 +2,10 @@
require 'spec_helper'
-RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :not_owned do
+RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :shared do
let_it_be(:admin) { create(:admin) }
describe 'DELETE /admin/sidekiq/queues/:queue_name' do
- context 'when the user is not an admin' do
- it 'returns a 403' do
- delete api("/admin/sidekiq/queues/authorized_projects?user=#{admin.username}", create(:user))
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'when the user is an admin' do
around do |example|
Sidekiq::Queue.new('authorized_projects').clear
@@ -31,14 +23,19 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category
end
context 'valid request' do
- it 'returns info about the deleted jobs' do
+ before do
add_job(admin, [1])
add_job(admin, [2])
add_job(create(:user), [3])
+ end
+
+ let_it_be(:path) { "/admin/sidekiq/queues/authorized_projects?user=#{admin.username}&worker_class=AuthorizedProjectsWorker" }
- delete api("/admin/sidekiq/queues/authorized_projects?user=#{admin.username}&worker_class=AuthorizedProjectsWorker", admin)
+ it_behaves_like 'DELETE request permissions for admin mode', success_status_code: :ok
+
+ it 'returns info about the deleted jobs' do
+ delete api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq('completed' => true,
'deleted_jobs' => 2,
'queue_size' => 1)
@@ -47,7 +44,7 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category
context 'when no required params are provided' do
it 'returns a 400' do
- delete api("/admin/sidekiq/queues/authorized_projects?user_2=#{admin.username}", admin)
+ delete api("/admin/sidekiq/queues/authorized_projects?user_2=#{admin.username}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -55,7 +52,7 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category
context 'when the queue does not exist' do
it 'returns a 404' do
- delete api("/admin/sidekiq/queues/authorized_projects_2?user=#{admin.username}", admin)
+ delete api("/admin/sidekiq/queues/authorized_projects_2?user=#{admin.username}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
index 21f3691c20b..7268fa2c90b 100644
--- a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
+++ b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :not_owned do
+RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :shared do
let(:user) { create(:admin) }
it 'is loaded' do
diff --git a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
index 77498c2e2b3..4a993d0b255 100644
--- a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
+++ b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :not_owned do
+RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
it 'is loaded' do
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 35851fff6c8..d5ad0779bd9 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::API, feature_category: :authentication_and_authorization do
+RSpec.describe API::API, feature_category: :system_access do
include GroupAPIHelpers
describe 'Record user last activity in after hook' do
diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb
index c08ecae28e8..3550e51d585 100644
--- a/spec/requests/api/appearance_spec.rb
+++ b/spec/requests/api/appearance_spec.rb
@@ -36,7 +36,9 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
end
describe "PUT /application/appearance" do
- it_behaves_like 'PUT request permissions for admin mode', { title: "Test" }
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { title: "Test" } }
+ end
context 'as an admin user' do
context "instance basics" do
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index b81cdcfea8e..5b07bded82c 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Applications, :api, feature_category: :authentication_and_authorization do
+RSpec.describe API::Applications, :api, feature_category: :system_access do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:scopes) { 'api' }
@@ -10,7 +10,9 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) }
describe 'POST /applications' do
- it_behaves_like 'POST request permissions for admin mode', { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } }
+ end
context 'authenticated and authorized user' do
it 'creates and returns an OAuth application' do
@@ -22,7 +24,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
expect(json_response).to be_a Hash
expect(json_response['application_id']).to eq application.uid
- expect(json_response['secret']).to eq application.secret
+ expect(application.secret_matches?(json_response['secret'])).to eq(true)
expect(json_response['callback_url']).to eq application.redirect_uri
expect(json_response['confidential']).to eq application.confidential
expect(application.scopes.to_s).to eq('api')
diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb
index fcef5b6ca78..0a77b6e228e 100644
--- a/spec/requests/api/avatar_spec.rb
+++ b/spec/requests/api/avatar_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe API::Avatar, feature_category: :user_profile do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}")
+ is_expected.to have_request_urgency(:medium)
end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 87dc06b7d15..22c67a253e3 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AwardEmoji, feature_category: :not_owned do
+RSpec.describe API::AwardEmoji, feature_category: :shared do
let_it_be_with_reload(:project) { create(:project, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index ee390773f29..7cea744cdb9 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
context 'when project is public with artifacts that are non public' do
- let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it 'rejects access to artifacts' do
project.update_column(:visibility_level,
@@ -439,7 +439,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when public project guest and artifacts are non public' do
let(:api_user) { guest }
- let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
before do
project.update_column(:visibility_level,
@@ -644,7 +644,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
context 'when project is public with non public artifacts' do
- let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) }
+ let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline, user: api_user) }
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
let(:public_builds) { true }
diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb
index 2a2c5f65aee..d760e4ddf28 100644
--- a/spec/requests/api/ci/pipeline_schedules_spec.rb
+++ b/spec/requests/api/ci/pipeline_schedules_spec.rb
@@ -473,12 +473,12 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra
end
context 'as the existing owner of the schedule' do
- it 'rejects the request and leaves the schedule unchanged' do
+ it 'accepts the request and leaves the schedule unchanged' do
expect do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer)
end.not_to change { pipeline_schedule.reload.owner }
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:success)
end
end
end
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 6d69da85449..4e81a052ecf 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
it_behaves_like 'pipelines visibility table'
context 'authorized user' do
- it 'returns project pipelines' do
+ it 'returns project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -52,7 +52,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
create(:ci_pipeline, project: project, status: target)
end
- it 'returns matched pipelines' do
+ it 'returns matched pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines", user), params: { scope: target }
expect(response).to have_gitlab_http_status(:ok)
@@ -307,7 +307,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return project pipelines' do
+ it 'does not return project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -335,13 +335,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'authorized user' do
- it 'returns pipeline jobs' do
+ it 'returns pipeline jobs', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
- it 'returns correct values' do
+ it 'returns correct values', :aggregate_failures do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
@@ -354,7 +354,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
end
- it 'returns pipeline data' do
+ it 'returns pipeline data', :aggregate_failures do
json_job = json_response.first
expect(json_job['pipeline']).not_to be_empty
@@ -368,7 +368,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'filter jobs with one scope element' do
let(:query) { { 'scope' => 'pending' } }
- it do
+ it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -382,7 +382,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when filtering to only running jobs' do
let(:query) { { 'scope' => 'running' } }
- it do
+ it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -402,7 +402,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'filter jobs with array of scope elements' do
let(:query) { { scope: %w(pending running) } }
- it do
+ it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
end
@@ -442,7 +442,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:successor) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
- it 'does not return retried jobs by default' do
+ it 'does not return retried jobs by default', :aggregate_failures do
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -450,7 +450,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when include_retried is false' do
let(:query) { { include_retried: false } }
- it 'does not return retried jobs' do
+ it 'does not return retried jobs', :aggregate_failures do
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -459,7 +459,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when include_retried is true' do
let(:query) { { include_retried: true } }
- it 'returns retried jobs' do
+ it 'returns retried jobs', :aggregate_failures do
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response[0]['name']).to eq(json_response[1]['name'])
@@ -469,7 +469,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'no pipeline is found' do
- it 'does not return jobs' do
+ it 'does not return jobs', :aggregate_failures do
get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
expect(json_response['message']).to eq '404 Project Not Found'
@@ -481,7 +481,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when user is not logged in' do
let(:api_user) { nil }
- it 'does not return jobs' do
+ it 'does not return jobs', :aggregate_failures do
expect(json_response['message']).to eq '404 Project Not Found'
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -523,13 +523,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'authorized user' do
- it 'returns pipeline bridges' do
+ it 'returns pipeline bridges', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
- it 'returns correct values' do
+ it 'returns correct values', :aggregate_failures do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(json_response.first['id']).to eq bridge.id
@@ -537,7 +537,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(json_response.first['stage']).to eq bridge.stage
end
- it 'returns pipeline data' do
+ it 'returns pipeline data', :aggregate_failures do
json_bridge = json_response.first
expect(json_bridge['pipeline']).not_to be_empty
@@ -548,7 +548,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
end
- it 'returns downstream pipeline data' do
+ it 'returns downstream pipeline data', :aggregate_failures do
json_bridge = json_response.first
expect(json_bridge['downstream_pipeline']).not_to be_empty
@@ -568,7 +568,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'with one scope element' do
let(:query) { { 'scope' => 'pending' } }
- it :skip_before_request do
+ it :skip_before_request, :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
expect(response).to have_gitlab_http_status(:ok)
@@ -581,7 +581,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'with array of scope elements' do
let(:query) { { scope: %w(pending running) } }
- it :skip_before_request do
+ it :skip_before_request, :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
expect(response).to have_gitlab_http_status(:ok)
@@ -635,7 +635,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'no pipeline is found' do
- it 'does not return bridges' do
+ it 'does not return bridges', :aggregate_failures do
get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
expect(json_response['message']).to eq '404 Project Not Found'
@@ -647,7 +647,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when user is not logged in' do
let(:api_user) { nil }
- it 'does not return bridges' do
+ it 'does not return bridges', :aggregate_failures do
expect(json_response['message']).to eq '404 Project Not Found'
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -704,7 +704,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
stub_ci_pipeline_to_return_yaml_file
end
- it 'creates and returns a new pipeline' do
+ it 'creates and returns a new pipeline', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch }
end.to change { project.ci_pipelines.count }.by(1)
@@ -717,7 +717,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'variables given' do
let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
- it 'creates and returns a new pipeline using the given variables' do
+ it 'creates and returns a new pipeline using the given variables', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables }
end.to change { project.ci_pipelines.count }.by(1)
@@ -738,7 +738,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
stub_ci_pipeline_yaml_file(config)
end
- it 'creates and returns a new pipeline using the given variables' do
+ it 'creates and returns a new pipeline using the given variables', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables }
end.to change { project.ci_pipelines.count }.by(1)
@@ -763,7 +763,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
- it 'fails when using an invalid ref' do
+ it 'fails when using an invalid ref', :aggregate_failures do
post api("/projects/#{project.id}/pipeline", user), params: { ref: 'invalid_ref' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -778,7 +778,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
project.update!(auto_devops_attributes: { enabled: false })
end
- it 'fails to create pipeline' do
+ it 'fails to create pipeline', :aggregate_failures do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -790,7 +790,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not create pipeline' do
+ it 'does not create pipeline', :aggregate_failures do
post api("/projects/#{project.id}/pipeline", non_member), params: { ref: project.default_branch }
expect(response).to have_gitlab_http_status(:not_found)
@@ -811,21 +811,21 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'authorized user' do
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/pipeline/detail')
end
- it 'returns project pipeline' do
+ it 'returns project pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['sha']).to match(/\A\h{40}\z/)
end
- it 'returns 404 when it does not exist' do
+ it 'returns 404 when it does not exist', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
@@ -847,7 +847,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return a project pipeline' do
+ it 'does not return a project pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -863,7 +863,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
create(:ci_pipeline, source: dangling_source, project: project)
end
- it 'returns the specified pipeline' do
+ it 'returns the specified pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{dangling_pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -887,7 +887,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'default repository branch' do
- it 'gets the latest pipleine' do
+ it 'gets the latest pipleine', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/latest", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -898,7 +898,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'ref parameter' do
- it 'gets the latest pipleine' do
+ it 'gets the latest pipleine', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/latest", user), params: { ref: second_branch.name }
expect(response).to have_gitlab_http_status(:ok)
@@ -910,7 +910,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return a project pipeline' do
+ it 'does not return a project pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -926,7 +926,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:api_user) { user }
context 'user is a mantainer' do
- it 'returns pipeline variables empty' do
+ it 'returns pipeline variables empty', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -936,7 +936,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'with variables' do
let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
- it 'returns pipeline variables' do
+ it 'returns pipeline variables', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -962,7 +962,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:api_user) { pipeline_owner_user }
let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
- it 'returns pipeline variables' do
+ it 'returns pipeline variables', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -987,7 +987,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'user is not a project member' do
- it 'does not return pipeline variables' do
+ it 'does not return pipeline variables', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1000,14 +1000,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'authorized user' do
let(:owner) { project.first_owner }
- it 'destroys the pipeline' do
+ it 'destroys the pipeline', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
expect(response).to have_gitlab_http_status(:no_content)
expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'returns 404 when it does not exist' do
+ it 'returns 404 when it does not exist', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", owner)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1021,7 +1021,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when the pipeline has jobs' do
let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) }
- it 'destroys associated jobs' do
+ it 'destroys associated jobs', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
expect(response).to have_gitlab_http_status(:no_content)
@@ -1044,7 +1044,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'unauthorized user' do
context 'when user is not member' do
- it 'returns a 404' do
+ it 'returns a 404', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1059,7 +1059,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
project.add_developer(developer)
end
- it 'returns a 403' do
+ it 'returns a 403', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1078,7 +1078,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) }
- it 'retries failed builds' do
+ it 'retries failed builds', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
end.to change { pipeline.builds.count }.from(1).to(2)
@@ -1089,7 +1089,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return a project pipeline' do
+ it 'does not return a project pipeline', :aggregate_failures do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1106,7 +1106,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
- it 'returns error' do
+ it 'returns error', :aggregate_failures do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1124,7 +1124,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
- context 'authorized user' do
+ context 'authorized user', :aggregate_failures do
it 'retries failed builds', :sidekiq_might_not_need_inline do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
@@ -1140,7 +1140,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
project.add_reporter(reporter)
end
- it 'rejects the action' do
+ it 'rejects the action', :aggregate_failures do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1156,7 +1156,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline does not have a test report' do
- it 'returns an empty test report' do
+ it 'returns an empty test report', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1167,7 +1167,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when pipeline has a test report' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
- it 'returns the test report' do
+ it 'returns the test report', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1180,7 +1180,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
create(:ci_build, :broken_test_reports, name: 'rspec', pipeline: pipeline)
end
- it 'returns a suite_error' do
+ it 'returns a suite_error', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1190,7 +1190,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return project pipelines' do
+ it 'does not return project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1208,7 +1208,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline does not have a test report summary' do
- it 'returns an empty test report summary' do
+ it 'returns an empty test report summary', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1219,7 +1219,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when pipeline has a test report summary' do
let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) }
- it 'returns the test report summary' do
+ it 'returns the test report summary', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1229,7 +1229,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return project pipelines' do
+ it 'does not return project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report_summary", non_member)
expect(response).to have_gitlab_http_status(:not_found)
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index ef3b38e3fc4..bf28b25e0a6 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -43,6 +43,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
.and change { runner_machine.reload.contacted_at }
end
+ context 'when runner_machine_heartbeat is disabled' do
+ before do
+ stub_feature_flags(runner_machine_heartbeat: false)
+ end
+
+ it 'does not load runner machine' do
+ queries = ActiveRecord::QueryRecorder.new { update_job(state: 'success') }
+ expect(queries.log).not_to include(/ci_runner_machines/)
+ end
+ end
+
context 'when status is given' do
it 'marks job as succeeded' do
update_job(state: 'success')
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 6e721d40560..28dbc4fd168 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -831,19 +831,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
end
end
-
- context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
- before do
- stub_feature_flags(ci_hooks_pre_get_sources_script: false)
- end
-
- it 'does not return the pre_get_sources_script' do
- request_job
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).not_to have_key('hooks')
- end
- end
end
describe 'port support' do
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index a6a1ad947aa..1b7dfe7706c 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
describe '/api/v4/runners' do
- describe 'POST /api/v4/runners/verify' do
+ describe 'POST /api/v4/runners/verify', :freeze_time do
let_it_be_with_reload(:runner) { create(:ci_runner, token_expires_at: 3.days.from_now) }
let(:params) {}
@@ -50,6 +50,30 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
stub_feature_flags(create_runner_machine: true)
end
+ context 'with glrt-prefixed token' do
+ let_it_be(:registration_token) { 'glrt-abcdefg123456' }
+ let_it_be(:registration_type) { :authenticated_user }
+ let_it_be(:runner) do
+ create(:ci_runner, registration_type: registration_type,
+ token: registration_token, token_expires_at: 3.days.from_now)
+ end
+
+ it 'verifies Runner credentials' do
+ verify
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'id' => runner.id,
+ 'token' => runner.token,
+ 'token_expires_at' => runner.token_expires_at.iso8601(3)
+ })
+ end
+
+ it 'does not update contacted_at' do
+ expect { verify }.not_to change { runner.reload.contacted_at }.from(nil)
+ end
+ end
+
it 'verifies Runner credentials' do
verify
@@ -61,6 +85,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
})
end
+ it 'updates contacted_at' do
+ expect { verify }.to change { runner.reload.contacted_at }.from(nil).to(Time.current)
+ end
+
context 'with non-expiring runner token' do
before do
runner.update!(token_expires_at: nil)
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
index 1110dbf5fbc..98edde93e95 100644
--- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
- subject { post api("#{prefix}/runners/reset_registration_token", user) }
+ let_it_be(:admin_mode) { false }
+
+ subject { post api("#{prefix}/runners/reset_registration_token", user, admin_mode: admin_mode) }
shared_examples 'bad request' do |result|
- it 'returns 400 error' do
+ it 'returns 400 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -15,7 +17,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_examples 'unauthenticated' do
- it 'returns 401 error' do
+ it 'returns 401 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -23,7 +25,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_examples 'unauthorized' do
- it 'returns 403 error' do
+ it 'returns 403 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -31,7 +33,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_examples 'not found' do |scope|
- it 'returns 404 error' do
+ it 'returns 404 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:not_found)
@@ -58,7 +60,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_context 'when authorized' do |scope|
- it 'resets runner registration token' do
+ it 'resets runner registration token', :aggregate_failures do
expect { subject }.to change { get_token }
expect(response).to have_gitlab_http_status(:success)
@@ -99,6 +101,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
include_context 'when authorized', 'instance' do
let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:admin_mode) { true }
def get_token
ApplicationSetting.current_without_cache.runners_registration_token
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index ca051386265..ec9b5621c37 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
describe 'GET /runners' do
context 'authorized user' do
- it 'returns response status and headers' do
+ it 'returns response status and headers', :aggregate_failures do
get api('/runners', user)
expect(response).to have_gitlab_http_status(:ok)
@@ -53,7 +53,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
]
end
- it 'filters runners by scope' do
+ it 'filters runners by scope', :aggregate_failures do
create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project])
get api('/runners?scope=paused', user)
@@ -112,7 +112,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'filters runners by tag_list' do
+ it 'filters runners by tag_list', :aggregate_failures do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
@@ -137,14 +137,14 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'authorized user' do
context 'with admin privileges' do
it 'returns response status and headers' do
- get api('/runners/all', admin)
+ get api('/runners/all', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
end
it 'returns all runners' do
- get api('/runners/all', admin)
+ get api('/runners/all', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner', 'is_shared' => false, 'active' => true, 'paused' => false, 'runner_type' => 'project_type'),
@@ -155,8 +155,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
]
end
- it 'filters runners by scope' do
- get api('/runners/all?scope=shared', admin)
+ it 'filters runners by scope', :aggregate_failures do
+ get api('/runners/all?scope=shared', admin, admin_mode: true)
shared = json_response.all? { |r| r['is_shared'] }
expect(response).to have_gitlab_http_status(:ok)
@@ -166,8 +166,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(shared).to be_truthy
end
- it 'filters runners by scope' do
- get api('/runners/all?scope=specific', admin)
+ it 'filters runners by scope', :aggregate_failures do
+ get api('/runners/all?scope=specific', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -181,12 +181,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'avoids filtering if scope is invalid' do
- get api('/runners/all?scope=unknown', admin)
+ get api('/runners/all?scope=unknown', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'filters runners by project type' do
- get api('/runners/all?type=project_type', admin)
+ get api('/runners/all?type=project_type', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner'),
@@ -195,7 +195,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'filters runners by group type' do
- get api('/runners/all?type=group_type', admin)
+ get api('/runners/all?type=group_type', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Group runner A'),
@@ -204,7 +204,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'does not filter by invalid type' do
- get api('/runners/all?type=bogus', admin)
+ get api('/runners/all?type=bogus', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -213,7 +213,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
let_it_be(:runner) { create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project]) }
it 'filters runners by status' do
- get api('/runners/all?paused=true', admin)
+ get api('/runners/all?paused=true', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Inactive project runner')
@@ -221,7 +221,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'filters runners by status' do
- get api('/runners/all?status=paused', admin)
+ get api('/runners/all?status=paused', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Inactive project runner')
@@ -230,16 +230,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'does not filter by invalid status' do
- get api('/runners/all?status=bogus', admin)
+ get api('/runners/all?status=bogus', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'filters runners by tag_list' do
+ it 'filters runners by tag_list', :aggregate_failures do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
- get api('/runners/all?tag_list=tag1,tag2', admin)
+ get api('/runners/all?tag_list=tag1,tag2', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
@@ -268,7 +268,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
describe 'GET /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it "returns runner's details" do
+ it "returns runner's details", :aggregate_failures do
get api("/runners/#{shared_runner.id}", admin)
expect(response).to have_gitlab_http_status(:ok)
@@ -284,39 +284,39 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when unused runner is present' do
let!(:unused_project_runner) { create(:ci_runner, :project, :without_projects) }
- it 'deletes unused runner' do
+ it 'deletes unused runner', :aggregate_failures do
expect do
- delete api("/runners/#{unused_project_runner.id}", admin)
+ delete api("/runners/#{unused_project_runner.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
end
end
- it "returns runner's details" do
- get api("/runners/#{project_runner.id}", admin)
+ it "returns runner's details", :aggregate_failures do
+ get api("/runners/#{project_runner.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(project_runner.description)
end
it "returns the project's details for a project runner" do
- get api("/runners/#{project_runner.id}", admin)
+ get api("/runners/#{project_runner.id}", admin, admin_mode: true)
expect(json_response['projects'].first['id']).to eq(project.id)
end
end
it 'returns 404 if runner does not exist' do
- get api('/runners/0', admin)
+ get api('/runners/0', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when the runner is a group runner' do
- it "returns the runner's details" do
- get api("/runners/#{group_runner_a.id}", admin)
+ it "returns the runner's details", :aggregate_failures do
+ get api("/runners/#{group_runner_a.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(group_runner_a.description)
@@ -326,7 +326,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context "runner project's administrative user" do
context 'when runner is not shared' do
- it "returns runner's details" do
+ it "returns runner's details", :aggregate_failures do
get api("/runners/#{project_runner.id}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -335,7 +335,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner is shared' do
- it "returns runner's details" do
+ it "returns runner's details", :aggregate_failures do
get api("/runners/#{shared_runner.id}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -373,7 +373,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(shared_runner.reload.description).to eq("#{description}_updated")
end
- it 'runner active state' do
+ it 'runner active state', :aggregate_failures do
active = shared_runner.active
update_runner(shared_runner.id, admin, active: !active)
@@ -381,7 +381,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(shared_runner.reload.active).to eq(!active)
end
- it 'runner paused state' do
+ it 'runner paused state', :aggregate_failures do
active = shared_runner.active
update_runner(shared_runner.id, admin, paused: active)
@@ -389,14 +389,14 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(shared_runner.reload.active).to eq(!active)
end
- it 'runner tag list' do
+ it 'runner tag list', :aggregate_failures do
update_runner(shared_runner.id, admin, tag_list: ['ruby2.1', 'pgsql', 'mysql'])
expect(response).to have_gitlab_http_status(:ok)
expect(shared_runner.reload.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
end
- it 'unrelated runner attribute on an existing runner with too many tags' do
+ it 'unrelated runner attribute on an existing runner with too many tags', :aggregate_failures do
# This test ensures that it is possible to update any attribute on a runner that currently fails the
# validation that ensures that there aren't too many tags associated with a runner
existing_invalid_shared_runner = build(:ci_runner, :instance, tag_list: (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" })
@@ -409,7 +409,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(existing_invalid_shared_runner.reload.active).to eq(!active)
end
- it 'runner untagged flag' do
+ it 'runner untagged flag', :aggregate_failures do
# Ensure tag list is non-empty before setting untagged to false.
update_runner(shared_runner.id, admin, tag_list: ['ruby2.1', 'pgsql', 'mysql'])
update_runner(shared_runner.id, admin, run_untagged: 'false')
@@ -418,28 +418,28 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(shared_runner.reload.run_untagged?).to be(false)
end
- it 'runner unlocked flag' do
+ it 'runner unlocked flag', :aggregate_failures do
update_runner(shared_runner.id, admin, locked: 'true')
expect(response).to have_gitlab_http_status(:ok)
expect(shared_runner.reload.locked?).to be(true)
end
- it 'runner access level' do
+ it 'runner access level', :aggregate_failures do
update_runner(shared_runner.id, admin, access_level: 'ref_protected')
expect(response).to have_gitlab_http_status(:ok)
expect(shared_runner.reload.ref_protected?).to be_truthy
end
- it 'runner maximum timeout' do
+ it 'runner maximum timeout', :aggregate_failures do
update_runner(shared_runner.id, admin, maximum_timeout: 1234)
expect(response).to have_gitlab_http_status(:ok)
expect(shared_runner.reload.maximum_timeout).to eq(1234)
end
- it 'fails with no parameters' do
+ it 'fails with no parameters', :aggregate_failures do
put api("/runners/#{shared_runner.id}", admin)
shared_runner.reload
@@ -448,7 +448,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner is shared' do
- it 'updates runner' do
+ it 'updates runner', :aggregate_failures do
description = shared_runner.description
active = shared_runner.active
runner_queue_value = shared_runner.ensure_runner_queue_value
@@ -476,7 +476,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner is not shared' do
- it 'updates runner' do
+ it 'updates runner', :aggregate_failures do
description = project_runner.description
runner_queue_value = project_runner.ensure_runner_queue_value
@@ -498,14 +498,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
def update_runner(id, user, args)
- put api("/runners/#{id}", user), params: args
+ put api("/runners/#{id}", user, admin_mode: true), params: args
end
end
context 'authorized user' do
+ let_it_be(:params) { { description: 'test' } }
+
context 'when runner is shared' do
it 'does not update runner' do
- put api("/runners/#{shared_runner.id}", user), params: { description: 'test' }
+ put api("/runners/#{shared_runner.id}", user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -518,12 +520,11 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'updates project runner with access to it' do
+ it 'updates project runner with access to it', :aggregate_failures do
description = project_runner.description
- put api("/runners/#{project_runner.id}", admin), params: { description: 'test' }
+ put api("/runners/#{project_runner.id}", admin, admin_mode: true), params: params
project_runner.reload
- expect(response).to have_gitlab_http_status(:ok)
expect(project_runner.description).to eq('test')
expect(project_runner.description).not_to eq(description)
end
@@ -542,13 +543,13 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
describe 'DELETE /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it 'deletes runner' do
+ it 'deletes runner', :aggregate_failures do
expect_next_instance_of(Ci::Runners::UnregisterRunnerService, shared_runner, admin) do |service|
expect(service).to receive(:execute).once.and_call_original
end
expect do
- delete api("/runners/#{shared_runner.id}", admin)
+ delete api("/runners/#{shared_runner.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.instance_type.count }.by(-1)
@@ -560,25 +561,25 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner is not shared' do
- it 'deletes used project runner' do
+ it 'deletes used project runner', :aggregate_failures do
expect_next_instance_of(Ci::Runners::UnregisterRunnerService, project_runner, admin) do |service|
expect(service).to receive(:execute).once.and_call_original
end
expect do
- delete api("/runners/#{project_runner.id}", admin)
+ delete api("/runners/#{project_runner.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
end
end
- it 'returns 404 if runner does not exist' do
+ it 'returns 404 if runner does not exist', :aggregate_failures do
allow_next_instance_of(Ci::Runners::UnregisterRunnerService) do |service|
expect(service).not_to receive(:execute)
end
- delete api('/runners/0', admin)
+ delete api('/runners/0', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -603,7 +604,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'deletes project runner for one owned project' do
+ it 'deletes project runner for one owned project', :aggregate_failures do
expect do
delete api("/runners/#{project_runner.id}", user)
@@ -658,7 +659,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'unauthorized user' do
- it 'does not delete project runner' do
+ it 'does not delete project runner', :aggregate_failures do
allow_next_instance_of(Ci::Runners::UnregisterRunnerService) do |service|
expect(service).not_to receive(:execute)
end
@@ -672,31 +673,31 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
describe 'POST /runners/:id/reset_authentication_token' do
context 'admin user' do
- it 'resets shared runner authentication token' do
+ it 'resets shared runner authentication token', :aggregate_failures do
expect do
- post api("/runners/#{shared_runner.id}/reset_authentication_token", admin)
+ post api("/runners/#{shared_runner.id}/reset_authentication_token", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq({ 'token' => shared_runner.reload.token, 'token_expires_at' => nil })
end.to change { shared_runner.reload.token }
end
- it 'returns 404 if runner does not exist' do
- post api('/runners/0/reset_authentication_token', admin)
+ it 'returns 404 if runner does not exist', :aggregate_failures do
+ post api('/runners/0/reset_authentication_token', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authorized user' do
- it 'does not reset project runner authentication token without access to it' do
+ it 'does not reset project runner authentication token without access to it', :aggregate_failures do
expect do
post api("/runners/#{project_runner.id}/reset_authentication_token", user2)
expect(response).to have_gitlab_http_status(:forbidden)
end.not_to change { project_runner.reload.token }
end
- it 'resets project runner authentication token for owned project' do
+ it 'resets project runner authentication token for owned project', :aggregate_failures do
expect do
post api("/runners/#{project_runner.id}/reset_authentication_token", user)
@@ -705,7 +706,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end.to change { project_runner.reload.token }
end
- it 'does not reset group runner authentication token with guest access' do
+ it 'does not reset group runner authentication token with guest access', :aggregate_failures do
expect do
post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_guest)
@@ -713,7 +714,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end.not_to change { group_runner_a.reload.token }
end
- it 'does not reset group runner authentication token with reporter access' do
+ it 'does not reset group runner authentication token with reporter access', :aggregate_failures do
expect do
post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_reporter)
@@ -721,7 +722,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end.not_to change { group_runner_a.reload.token }
end
- it 'does not reset group runner authentication token with developer access' do
+ it 'does not reset group runner authentication token with developer access', :aggregate_failures do
expect do
post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_developer)
@@ -729,7 +730,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end.not_to change { group_runner_a.reload.token }
end
- it 'does not reset group runner authentication token with maintainer access' do
+ it 'does not reset group runner authentication token with maintainer access', :aggregate_failures do
expect do
post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_maintainer)
@@ -737,7 +738,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end.not_to change { group_runner_a.reload.token }
end
- it 'resets group runner authentication token with owner access' do
+ it 'resets group runner authentication token with owner access', :aggregate_failures do
expect do
post api("/runners/#{group_runner_a.id}/reset_authentication_token", user)
@@ -746,7 +747,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end.to change { group_runner_a.reload.token }
end
- it 'resets group runner authentication token with owner access with expiration time', :freeze_time do
+ it 'resets group runner authentication token with owner access with expiration time', :aggregate_failures, :freeze_time do
expect(group_runner_a.reload.token_expires_at).to be_nil
group.update!(runner_token_expiration_interval: 5.days)
@@ -763,7 +764,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'unauthorized user' do
- it 'does not reset authentication token' do
+ it 'does not reset authentication token', :aggregate_failures do
expect do
post api("/runners/#{shared_runner.id}/reset_authentication_token")
@@ -783,8 +784,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'admin user' do
context 'when runner exists' do
context 'when runner is shared' do
- it 'return jobs' do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ it 'return jobs', :aggregate_failures do
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -795,8 +796,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner is a project runner' do
- it 'return jobs' do
- get api("/runners/#{project_runner.id}/jobs", admin)
+ it 'return jobs', :aggregate_failures do
+ get api("/runners/#{project_runner.id}/jobs", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -806,7 +807,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when user does not have authorization to see all jobs' do
- it 'shows only jobs it has permission to see' do
+ it 'shows only jobs it has permission to see', :aggregate_failures do
create(:ci_build, :running, runner: two_projects_runner, project: project)
create(:ci_build, :running, runner: two_projects_runner, project: project2)
@@ -824,8 +825,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when valid status is provided' do
- it 'return filtered jobs' do
- get api("/runners/#{project_runner.id}/jobs?status=failed", admin)
+ it 'return filtered jobs', :aggregate_failures do
+ get api("/runners/#{project_runner.id}/jobs?status=failed", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -838,8 +839,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when valid order_by is provided' do
context 'when sort order is not specified' do
- it 'return jobs in descending order' do
- get api("/runners/#{project_runner.id}/jobs?order_by=id", admin)
+ it 'return jobs in descending order', :aggregate_failures do
+ get api("/runners/#{project_runner.id}/jobs?order_by=id", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -851,8 +852,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when sort order is specified as asc' do
- it 'return jobs sorted in ascending order' do
- get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin)
+ it 'return jobs sorted in ascending order', :aggregate_failures do
+ get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -866,7 +867,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when invalid status is provided' do
it 'return 400' do
- get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -874,7 +875,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when invalid order_by is provided' do
it 'return 400' do
- get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -882,7 +883,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when invalid sort is provided' do
it 'return 400' do
- get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -890,16 +891,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'avoids N+1 DB queries' do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
control = ActiveRecord::QueryRecorder.new do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
end
create(:ci_build, :failed, runner: shared_runner, project: project)
expect do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
end.not_to exceed_query_limit(control.count)
end
@@ -925,12 +926,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
]).once.and_call_original
end
- get api("/runners/#{shared_runner.id}/jobs", admin), params: { per_page: 2, order_by: 'id', sort: 'desc' }
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true), params: { per_page: 2, order_by: 'id', sort: 'desc' }
end
context "when runner doesn't exist" do
it 'returns 404' do
- get api('/runners/0/jobs', admin)
+ get api('/runners/0/jobs', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -948,7 +949,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner is a project runner' do
- it 'return jobs' do
+ it 'return jobs', :aggregate_failures do
get api("/runners/#{project_runner.id}/jobs", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -960,7 +961,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when valid status is provided' do
- it 'return filtered jobs' do
+ it 'return filtered jobs', :aggregate_failures do
get api("/runners/#{project_runner.id}/jobs?status=failed", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1027,8 +1028,8 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
describe 'GET /projects/:id/runners' do
context 'authorized user with maintainer privileges' do
- it 'returns response status and headers' do
- get api('/runners/all', admin)
+ it 'returns response status and headers', :aggregate_failures do
+ get api('/runners/all', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1044,7 +1045,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
]
end
- it 'filters runners by scope' do
+ it 'filters runners by scope', :aggregate_failures do
get api("/projects/#{project.id}/runners?scope=specific", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1102,7 +1103,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'filters runners by tag_list' do
+ it 'filters runners by tag_list', :aggregate_failures do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
@@ -1183,7 +1184,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
end
- it 'filters runners by tag_list' do
+ it 'filters runners by tag_list', :aggregate_failures do
create(:ci_runner, :group, description: 'Runner tagged with tag1 and tag2', groups: [group], tag_list: %w[tag1 tag2])
create(:ci_runner, :group, description: 'Runner tagged with tag2', groups: [group], tag_list: %w[tag1])
@@ -1203,21 +1204,21 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
- it 'enables project runner' do
+ it 'enables project runner', :aggregate_failures do
expect do
post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id }
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
- it 'avoids changes when enabling already enabled runner' do
+ it 'avoids changes when enabling already enabled runner', :aggregate_failures do
expect do
post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner.id }
end.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'does not enable locked runner' do
+ it 'does not enable locked runner', :aggregate_failures do
project_runner2.update!(locked: true)
expect do
@@ -1243,9 +1244,9 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when project runner is used' do
let!(:new_project_runner) { create(:ci_runner, :project) }
- it 'enables any project runner' do
+ it 'enables any project runner', :aggregate_failures do
expect do
- post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
+ post api("/projects/#{project.id}/runners", admin, admin_mode: true), params: { runner_id: new_project_runner.id }
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
@@ -1255,9 +1256,9 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
end
- it 'does not enable project runner' do
+ it 'does not enable project runner', :aggregate_failures do
expect do
- post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
+ post api("/projects/#{project.id}/runners", admin, admin_mode: true), params: { runner_id: new_project_runner.id }
end.not_to change { project.runners.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1266,7 +1267,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'raises an error when no runner_id param is provided' do
- post api("/projects/#{project.id}/runners", admin)
+ post api("/projects/#{project.id}/runners", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1316,7 +1317,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
context 'when runner have one associated projects' do
- it "does not disable project's runner" do
+ it "does not disable project's runner", :aggregate_failures do
expect do
delete api("/projects/#{project.id}/runners/#{project_runner.id}", user)
end.to change { project.runners.count }.by(0)
diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb
index 0f9f1bc80d6..5ea9104cb15 100644
--- a/spec/requests/api/ci/variables_spec.rb
+++ b/spec/requests/api/ci/variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Variables, feature_category: :pipeline_authoring do
+RSpec.describe API::Ci::Variables, feature_category: :pipeline_composition do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index bcc27a80cf8..b4bc4507021 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -132,6 +132,42 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
it_behaves_like 'project commits'
end
+ context 'with author parameter' do
+ let(:params) { { author: 'Zaporozhets' } }
+
+ it 'returns only this author commits' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ author_names = json_response.map { |commit| commit['author_name'] }.uniq
+
+ expect(author_names).to contain_exactly('Dmitriy Zaporozhets')
+ end
+
+ context 'when author is missing' do
+ let(:params) { { author: '' } }
+
+ it 'returns all commits' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(20)
+ end
+ end
+
+ context 'when author does not exists' do
+ let(:params) { { author: 'does not exist' } }
+
+ it 'returns an empty list' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+ end
+
context 'when repository does not exist' do
let(:project) { create(:project, creator: user, path: 'my.project') }
@@ -425,6 +461,27 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project_id}/repository/commits" }
+ context 'when unauthenticated', 'and project is public' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:params) do
+ {
+ branch: 'master',
+ commit_message: 'message',
+ actions: [
+ {
+ action: 'create',
+ file_path: '/test.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it_behaves_like '401 response' do
+ let(:request) { post api(url), params: params }
+ end
+ end
+
it 'returns a 403 unauthorized for user without permissions' do
post api(url, guest)
@@ -523,7 +580,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
let(:property) { 'g_edit_by_web_ide' }
let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit' }
let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context] }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
context 'counts.web_ide_commits Snowplow event tracking' do
@@ -1776,7 +1832,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
- it_behaves_like '403 response' do
+ it_behaves_like '401 response' do
let(:request) { post api(route), params: { branch: 'master' } }
end
end
@@ -1956,7 +2012,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
- it_behaves_like '403 response' do
+ it_behaves_like '401 response' do
let(:request) { post api(route), params: { branch: branch } }
end
end
diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb
index 0c726d46a01..2bb2ffa03c4 100644
--- a/spec/requests/api/composer_packages_spec.rb
+++ b/spec/requests/api/composer_packages_spec.rb
@@ -504,7 +504,11 @@ RSpec.describe API::ComposerPackages, feature_category: :package_registry do
include_context 'Composer user type', params[:user_role], params[:member] do
if params[:expected_status] == :success
let(:snowplow_gitlab_standard_context) do
- { project: project, namespace: project.namespace, property: 'i_package_composer_user' }
+ if user_role == :anonymous || (project_visibility_level == 'PUBLIC' && user_token == false)
+ { project: project, namespace: project.namespace, property: 'i_package_composer_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_composer_user', user: user }
+ end
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 0c80b7d830f..25b99862100 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -31,9 +31,11 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file }
+ let(:target_component_name) { component.name }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
@@ -43,27 +45,37 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+ let(:target_component_file) { component_file_sources }
+ let(:target_component_name) { component.name }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_sources_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file_di }
+ let(:target_component_name) { component.name }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
@@ -73,9 +85,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_di_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
end
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
@@ -89,6 +104,7 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
'libsample0_1.2.3~alpha2_amd64.deb' | /^!<arch>/
'sample-udeb_1.2.3~alpha2_amd64.udeb' | /^!<arch>/
+ 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | /^!<arch>/
'sample_1.2.3~alpha2_amd64.buildinfo' | /Build-Tainted-By/
'sample_1.2.3~alpha2_amd64.changes' | /urgency=medium/
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index 46f79efd928..e9ad39a08ab 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -44,9 +44,11 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file }
+ let(:target_component_name) { component.name }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -57,30 +59,40 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/Sources' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+ let(:target_component_file) { component_file_sources }
+ let(:target_component_name) { component.name }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_sources_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file_di }
+ let(:target_component_name) { component.name }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -91,9 +103,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_di_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -108,6 +123,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
'libsample0_1.2.3~alpha2_amd64.deb' | /^!<arch>/
'sample-udeb_1.2.3~alpha2_amd64.udeb' | /^!<arch>/
+ 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | /^!<arch>/
'sample_1.2.3~alpha2_amd64.buildinfo' | /Build-Tainted-By/
'sample_1.2.3~alpha2_amd64.changes' | /urgency=medium/
end
@@ -162,7 +178,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
it_behaves_like "Debian packages upload request", :bad_request,
- /^file_name Only debs and udebs can be directly added to a distribution$/
+ /^file_name Only debs, udebs and ddebs can be directly added to a distribution$/
end
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 5116f074894..888220c2251 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'doorkeeper access', feature_category: :authentication_and_authorization do
+RSpec.describe 'doorkeeper access', feature_category: :system_access do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb
index e8f519e004d..d239853ac1d 100644
--- a/spec/requests/api/draft_notes_spec.rb
+++ b/spec/requests/api/draft_notes_spec.rb
@@ -8,11 +8,16 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :public) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:private_merge_request) do
+ create(:merge_request, source_project: private_project, target_project: private_project)
+ end
+
let_it_be(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
let!(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) }
let!(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) }
- let_it_be(:api_stub) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}" }
+ let_it_be(:base_url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes" }
before do
project.add_developer(user)
@@ -20,13 +25,13 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
describe "Get a list of merge request draft notes" do
it "returns 200 OK status" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+ get api(base_url, user)
expect(response).to have_gitlab_http_status(:ok)
end
it "returns only draft notes authored by the current user" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+ get api(base_url, user)
draft_note_ids = json_response.pluck("id")
@@ -40,7 +45,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when requesting an existing draft note by the user" do
before do
get api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}",
+ "#{base_url}/#{draft_note_by_current_user.id}",
user
)
end
@@ -56,7 +61,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when requesting a non-existent draft note" do
it "returns a 404 Not Found response" do
get api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{DraftNote.last.id + 1}",
+ "#{base_url}/#{DraftNote.last.id + 1}",
user
)
@@ -67,7 +72,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when requesting an existing draft note by another user" do
it "returns a 404 Not Found response" do
get api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}",
+ "#{base_url}/#{draft_note_by_random_user.id}",
user
)
@@ -83,7 +88,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
before do
delete api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}",
+ "#{base_url}/#{draft_note_by_current_user.id}",
user
)
end
@@ -100,7 +105,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when deleting a non-existent draft note" do
it "returns a 404 Not Found" do
delete api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{non_existing_record_id}",
+ "#{base_url}/#{non_existing_record_id}",
user
)
@@ -111,7 +116,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when deleting a draft note by a different user" do
it "returns a 404 Not Found" do
delete api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}",
+ "#{base_url}/#{draft_note_by_random_user.id}",
user
)
@@ -120,10 +125,152 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
end
end
+ def create_draft_note(params = {}, url = base_url)
+ post api(url, user), params: params
+ end
+
+ describe "Create a new draft note" do
+ let(:basic_create_params) do
+ {
+ note: "Example body string"
+ }
+ end
+
+ context "when creating a new draft note" do
+ context "with required params" do
+ it "returns 201 Created status" do
+ create_draft_note(basic_create_params)
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ it "creates a new draft note with the submitted params" do
+ expect { create_draft_note(basic_create_params) }.to change { DraftNote.count }.by(1)
+
+ expect(json_response["note"]).to eq(basic_create_params[:note])
+ expect(json_response["merge_request_id"]).to eq(merge_request.id)
+ expect(json_response["author_id"]).to eq(user.id)
+ end
+ end
+
+ context "without required params" do
+ it "returns 400 Bad Request status" do
+ create_draft_note({})
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "when providing a non-existing commit_id" do
+ it "returns a 400 Bad Request" do
+ create_draft_note(
+ basic_create_params.merge(
+ commit_id: 'bad SHA'
+ )
+ )
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "when targeting a merge request the user doesn't have access to" do
+ it "returns a 404 Not Found" do
+ create_draft_note(
+ basic_create_params,
+ "/projects/#{private_project.id}/merge_requests/#{private_merge_request.iid}"
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when attempting to resolve a disscussion" do
+ context "when providing a non-existant ID" do
+ it "returns a 400 Bad Request" do
+ create_draft_note(
+ basic_create_params.merge(
+ resolve_discussion: true,
+ in_reply_to_discussion_id: non_existing_record_id
+ )
+ )
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "when not providing an ID" do
+ it "returns a 400 Bad Request" do
+ create_draft_note(basic_create_params.merge(resolve_discussion: true))
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it "returns a validation error message" do
+ create_draft_note(basic_create_params.merge(resolve_discussion: true))
+
+ expect(response.body)
+ .to eq("{\"message\":{\"base\":[\"User is not allowed to resolve thread\"]}}")
+ end
+ end
+ end
+ end
+ end
+
+ def update_draft_note(params = {}, url = base_url)
+ put api("#{url}/#{draft_note_by_current_user.id}", user), params: params
+ end
+
+ describe "Update a draft note" do
+ let(:basic_update_params) do
+ {
+ note: "Example updated body string"
+ }
+ end
+
+ context "when updating an existing draft note" do
+ context "with required params" do
+ it "returns 200 Success status" do
+ update_draft_note(basic_update_params)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ it "updates draft note with the new content" do
+ update_draft_note(basic_update_params)
+
+ expect(json_response["note"]).to eq(basic_update_params[:note])
+ end
+ end
+
+ context "without including an update to the note body" do
+ it "returns the draft note with no changes" do
+ expect { update_draft_note({}) }
+ .not_to change { draft_note_by_current_user.note }
+ end
+ end
+
+ context "when updating a non-existent draft note" do
+ it "returns a 404 Not Found" do
+ put api("#{base_url}/#{non_existing_record_id}", user), params: basic_update_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when updating a draft note by a different user" do
+ it "returns a 404 Not Found" do
+ put api("#{base_url}/#{draft_note_by_random_user.id}", user), params: basic_update_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
describe "Publishing a draft note" do
let(:publish_draft_note) do
put api(
- "#{api_stub}/draft_notes/#{draft_note_by_current_user.id}/publish",
+ "#{base_url}/#{draft_note_by_current_user.id}/publish",
user
)
end
@@ -144,7 +291,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when publishing a non-existent draft note" do
it "returns a 404 Not Found" do
put api(
- "#{api_stub}/draft_notes/#{non_existing_record_id}/publish",
+ "#{base_url}/#{non_existing_record_id}/publish",
user
)
@@ -155,7 +302,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when publishing a draft note by a different user" do
it "returns a 404 Not Found" do
put api(
- "#{api_stub}/draft_notes/#{draft_note_by_random_user.id}/publish",
+ "#{base_url}/#{draft_note_by_random_user.id}/publish",
user
)
diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb
index 5906cdf105a..3b01dec6f9c 100644
--- a/spec/requests/api/error_tracking/project_settings_spec.rb
+++ b/spec/requests/api/error_tracking/project_settings_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
end
- shared_examples 'returns 404' do
+ shared_examples 'returns no project settings' do
it 'returns no project settings' do
make_request
@@ -48,6 +48,57 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
end
+ shared_examples 'returns 400' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ shared_examples 'returns 401' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ shared_examples 'returns 403' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ shared_examples 'returns 404' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'returns 400 with `integrated` param required or invalid' do |error|
+ it 'returns 400' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error'])
+ .to eq(error)
+ end
+ end
+
+ shared_examples "returns error from UpdateService" do
+ it "returns errors" do
+ make_request
+
+ expect(json_response['http_status']).to eq('forbidden')
+ expect(json_response['message']).to eq('An error occurred')
+ end
+ end
+
describe "PATCH /projects/:id/error_tracking/settings" do
let(:params) { { active: false } }
@@ -127,14 +178,34 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'without a project setting' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
before do
project.add_maintainer(user)
end
context 'patch settings' do
- it_behaves_like 'returns 404'
+ it_behaves_like 'returns no project settings'
+ end
+ end
+
+ context "when ::Projects::Operations::UpdateService responds with an error" do
+ before do
+ allow_next_instance_of(::Projects::Operations::UpdateService) do |service|
+ allow(service)
+ .to receive(:execute)
+ .and_return({ status: :error, message: 'An error occurred', http_status: :forbidden })
+ end
+ end
+
+ context "when integrated" do
+ let(:integrated) { true }
+
+ it_behaves_like 'returns error from UpdateService'
+ end
+
+ context "without integrated" do
+ it_behaves_like 'returns error from UpdateService'
end
end
end
@@ -145,11 +216,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'patch request' do
- it 'returns 403' do
- make_request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'returns 403'
end
end
@@ -159,21 +226,13 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'patch request' do
- it 'returns 403' do
- make_request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'returns 403'
end
end
context 'when authenticated as non-member' do
context 'patch request' do
- it 'returns 404' do
- make_request
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'returns 404'
end
end
@@ -181,11 +240,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
let(:user) { nil }
context 'patch request' do
- it 'returns 401 for update request' do
- make_request
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ it_behaves_like 'returns 401'
end
end
end
@@ -227,7 +282,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
context 'get settings' do
- it_behaves_like 'returns 404'
+ it_behaves_like 'returns no project settings'
end
end
@@ -236,11 +291,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
project.add_reporter(user)
end
- it 'returns 403' do
- make_request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'returns 403'
end
context 'when authenticated as developer' do
@@ -248,29 +299,138 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
project.add_developer(user)
end
- it 'returns 403' do
- make_request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'returns 403'
end
context 'when authenticated as non-member' do
- it 'returns 404' do
- make_request
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'returns 404'
end
context 'when unauthenticated' do
let(:user) { nil }
- it 'returns 401' do
- make_request
+ it_behaves_like 'returns 401'
+ end
+ end
+
+ describe "PUT /projects/:id/error_tracking/settings" do
+ let(:params) { { active: active, integrated: integrated } }
+ let(:active) { true }
+ let(:integrated) { true }
+
+ def make_request
+ put api("/projects/#{project.id}/error_tracking/settings", user), params: params
+ end
+
+ context 'when authenticated' do
+ context 'as maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context "when integrated" do
+ let(:integrated) { true }
+
+ context "with existing setting" do
+ let(:setting) { create(:project_error_tracking_setting, :integrated) }
+ let(:active) { false }
+
+ it "updates a setting" do
+ expect { make_request }.not_to change { ErrorTracking::ProjectErrorTrackingSetting.count }
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response).to eq(
+ "active" => false,
+ "api_url" => nil,
+ "integrated" => integrated,
+ "project_name" => nil,
+ "sentry_external_url" => nil
+ )
+ end
+ end
+
+ context "without setting" do
+ let(:active) { true }
+ let_it_be(:project) { create(:project) }
+
+ it "creates a setting" do
+ expect { make_request }.to change { ErrorTracking::ProjectErrorTrackingSetting.count }
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response).to eq(
+ "active" => true,
+ "api_url" => nil,
+ "integrated" => integrated,
+ "project_name" => nil,
+ "sentry_external_url" => nil
+ )
+ end
+ end
+
+ context "when ::Projects::Operations::UpdateService responds with an error" do
+ before do
+ allow_next_instance_of(::Projects::Operations::UpdateService) do |service|
+ allow(service)
+ .to receive(:execute)
+ .and_return({ status: :error, message: 'An error occurred', http_status: :forbidden })
+ end
+ end
+
+ it_behaves_like 'returns error from UpdateService'
+ end
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
+ context "integrated_error_tracking feature disabled" do
+ let(:integrated) { true }
+
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
+ end
+
+ it_behaves_like 'returns 404'
+ end
+
+ context "when integrated param is invalid" do
+ let(:params) { { active: active, integrated: 'invalid_string' } }
+
+ it_behaves_like 'returns 400 with `integrated` param required or invalid', 'integrated is invalid'
+ end
+
+ context "when integrated param is missing" do
+ let(:params) { { active: active } }
+
+ it_behaves_like 'returns 400 with `integrated` param required or invalid', 'integrated is missing'
+ end
+ end
+
+ context 'as reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'returns 403'
+ end
+
+ context "as developer" do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'returns 403'
end
+
+ context 'as non-member' do
+ it_behaves_like 'returns 404'
+ end
+ end
+
+ context "when unauthorized" do
+ let(:user) { nil }
+ let(:integrated) { true }
+
+ it_behaves_like 'returns 401'
end
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index f4066c54c47..ed84e3e5f48 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -55,6 +55,11 @@ RSpec.describe API::Files, feature_category: :source_code_management do
}
end
+ let(:last_commit_for_path) do
+ Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
+ end
+
shared_context 'with author parameters' do
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
@@ -136,6 +141,12 @@ RSpec.describe API::Files, feature_category: :source_code_management do
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
head api(route(file_path), current_user, **options), params: params
+ expect(Gitlab::Cache::Client).to receive(:build_with_metadata).with(
+ cache_identifier: 'API::Files#content_sha',
+ feature_category: :source_code_management,
+ backing_resource: :gitaly
+ ).and_call_original
+
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
.to eq(content_sha256)
@@ -829,7 +840,6 @@ RSpec.describe API::Files, feature_category: :source_code_management do
expect_to_send_git_blob(api(url, current_user), params)
expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache')
- expect(response.headers['Pragma']).to eq('no-cache')
expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT')
end
@@ -1180,7 +1190,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
context 'when updating an existing file with stale last commit id' do
- let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') }
+ let(:params_with_stale_id) { params.merge(last_commit_id: last_commit_for_path.parent_id) }
it 'returns a 400 bad request' do
put api(route(file_path), user), params: params_with_stale_id
@@ -1191,12 +1201,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
context 'with correct last commit id' do
- let(:last_commit) do
- Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- end
-
- let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) }
it 'updates existing file in project repo' do
put api(route(file_path), user), params: params_with_correct_id
@@ -1206,12 +1211,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
context 'when file path is invalid' do
- let(:last_commit) do
- Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- end
-
- let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) }
it 'returns a 400 bad request' do
put api(route(invalid_file_path), user), params: params_with_correct_id
@@ -1222,12 +1222,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
it_behaves_like 'when path is absolute' do
- let(:last_commit) do
- Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- end
-
- let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) }
subject { put api(route(absolute_path), user), params: params_with_correct_id }
end
diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb
index 170871706dc..a53db516940 100644
--- a/spec/requests/api/freeze_periods_spec.rb
+++ b/spec/requests/api/freeze_periods_spec.rb
@@ -12,14 +12,12 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let(:last_freeze_period) { project.freeze_periods.last }
describe 'GET /projects/:id/freeze_periods' do
+ let_it_be(:path) { "/projects/#{project.id}/freeze_periods" }
+
context 'when the user is the admin' do
let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
- it 'returns 200 HTTP status' do
- get api("/projects/#{project.id}/freeze_periods", admin)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it_behaves_like 'GET request permissions for admin mode when admin', :not_found
end
context 'when the user is the maintainer' do
@@ -31,36 +29,26 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
- it 'returns 200 HTTP status' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ it 'returns freeze_periods ordered by created_at ascending', :aggregate_failures do
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'returns freeze_periods ordered by created_at ascending' do
- get api("/projects/#{project.id}/freeze_periods", user)
-
expect(json_response.count).to eq(2)
expect(freeze_period_ids).to eq([freeze_period_1.id, freeze_period_2.id])
end
it 'matches response schema' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/freeze_periods')
end
end
context 'when there are no freeze_periods' do
- it 'returns 200 HTTP status' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ it 'returns 200 HTTP status with empty response', :aggregate_failures do
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'returns an empty response' do
- get api("/projects/#{project.id}/freeze_periods", user)
-
expect(json_response).to be_empty
end
end
@@ -75,28 +63,21 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
create(:ci_freeze_period, project: project)
end
- it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'GET request permissions for admin mode when user', :forbidden do
+ let(:current_user) { user }
+ end
end
end
context 'when user is not a project member' do
- it 'responds 404 Not Found' do
- get api("/projects/#{project.id}/freeze_periods", user)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'GET request permissions for admin mode when user', :not_found
context 'when project is public' do
let(:project) { create(:project, :public) }
+ let(:path) { "/projects/#{project.id}/freeze_periods" }
- it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'GET request permissions for admin mode when user', :forbidden
end
end
end
@@ -107,14 +88,12 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
create(:ci_freeze_period, project: project)
end
+ let(:path) { "/projects/#{project.id}/freeze_periods/#{freeze_period.id}" }
+
context 'when the user is the admin' do
let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
- it 'responds 200 OK' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", admin)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it_behaves_like 'GET request permissions for admin mode when admin', :not_found
end
context 'when the user is the maintainer' do
@@ -122,15 +101,10 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
project.add_maintainer(user)
end
- it 'responds 200 OK' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ it 'returns a freeze period', :aggregate_failures do
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'returns a freeze period' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
-
expect(json_response).to include(
'id' => freeze_period.id,
'freeze_start' => freeze_period.freeze_start,
@@ -139,7 +113,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
end
it 'matches response schema' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/freeze_period')
end
@@ -150,28 +124,26 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
project.add_guest(user)
end
- it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'GET request permissions for admin mode when user' do
+ let(:current_user) { user }
+ end
end
context 'when project is public' do
let(:project) { create(:project, :public) }
- context 'when freeze_period exists' do
- it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden when freeze_period exists' do
+ it_behaves_like 'GET request permissions for admin mode when user' do
+ let(:current_user) { user }
end
end
- context 'when freeze_period does not exist' do
- it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods/0", user)
+ context 'and responds 403 Forbidden when freeze_period does not exist' do
+ let(:path) { "/projects/#{project.id}/freeze_periods/0" }
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'GET request permissions for admin mode when user' do
+ let(:current_user) { user }
end
end
end
@@ -188,15 +160,13 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
}
end
- subject { post api("/projects/#{project.id}/freeze_periods", api_user), params: params }
+ let(:path) { "/projects/#{project.id}/freeze_periods" }
- context 'when the user is the admin' do
- let(:api_user) { admin }
-
- it 'accepts the request' do
- subject
+ subject { post api(path, api_user), params: params }
- expect(response).to have_gitlab_http_status(:created)
+ context 'when the user is the admin' do
+ it_behaves_like 'POST request permissions for admin mode when admin', :not_found do
+ let(:current_user) { admin }
end
end
@@ -212,7 +182,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
expect(response).to have_gitlab_http_status(:created)
end
- it 'creates a new freeze period' do
+ it 'creates a new freeze period', :aggregate_failures do
expect do
subject
end.to change { Ci::FreezePeriod.count }.by(1)
@@ -268,10 +238,10 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
project.add_developer(user)
end
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'POST request permissions for admin mode when user' do
+ let(:current_user) { user }
+ end
end
end
@@ -280,28 +250,22 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
project.add_reporter(user)
end
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'POST request permissions for admin mode when user' do
+ let(:current_user) { user }
+ end
end
end
context 'when user is not a project member' do
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'POST request permissions for admin mode when user', :not_found
end
context 'when project is public' do
let(:project) { create(:project, :public) }
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'POST request permissions for admin mode when user', :forbidden
end
end
end
@@ -309,17 +273,12 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
describe 'PUT /projects/:id/freeze_periods/:freeze_period_id' do
let(:params) { { freeze_start: '0 22 * * 5', freeze_end: '5 4 * * sun' } }
let!(:freeze_period) { create :ci_freeze_period, project: project }
+ let(:path) { "/projects/#{project.id}/freeze_periods/#{freeze_period.id}" }
- subject { put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user), params: params }
+ subject { put api(path, api_user), params: params }
context 'when user is the admin' do
- let(:api_user) { admin }
-
- it 'accepts the request' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it_behaves_like 'PUT request permissions for admin mode when admin', :not_found
end
context 'when user is the maintainer' do
@@ -367,27 +326,23 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
project.add_reporter(user)
end
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'PUT request permissions for admin mode when user' do
+ let(:current_user) { user }
+ end
end
end
context 'when user is not a project member' do
- it 'responds 404 Not Found' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
+ context 'and responds 404 Not Found' do
+ it_behaves_like 'PUT request permissions for admin mode when user', :not_found
end
context 'when project is public' do
let(:project) { create(:project, :public) }
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'PUT request permissions for admin mode when user'
end
end
end
@@ -396,17 +351,12 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
describe 'DELETE /projects/:id/freeze_periods/:freeze_period_id' do
let!(:freeze_period) { create :ci_freeze_period, project: project }
let(:freeze_period_id) { freeze_period.id }
+ let(:path) { "/projects/#{project.id}/freeze_periods/#{freeze_period_id}" }
- subject { delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user) }
+ subject { delete api(path, api_user) }
context 'when user is the admin' do
- let(:api_user) { admin }
-
- it 'accepts the request' do
- subject
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
+ it_behaves_like 'DELETE request permissions for admin mode when admin', failed_status_code: :not_found
end
context 'when user is the maintainer' do
@@ -442,27 +392,23 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
project.add_reporter(user)
end
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'DELETE request permissions for admin mode when user' do
+ let(:current_user) { user }
+ end
end
end
context 'when user is not a project member' do
- it 'responds 404 Not Found' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
+ context 'and responds 404 Not Found' do
+ it_behaves_like 'DELETE request permissions for admin mode when user', :not_found
end
context 'when project is public' do
let(:project) { create(:project, :public) }
- it 'responds 403 Forbidden' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'and responds 403 Forbidden' do
+ it_behaves_like 'DELETE request permissions for admin mode when user'
end
end
end
diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..dc19f3bdc91
--- /dev/null
+++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement1) { create(:user_achievement, achievement: achievement, user: user) }
+ let_it_be(:user_achievement2) { create(:user_achievement, :revoked, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ id
+ achievements {
+ nodes {
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('namespace', { full_path: group.full_path }, fields)
+ end
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all user_achievements' do
+ expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes))
+ .to contain_exactly(
+ a_graphql_entity_for(user_achievement1),
+ a_graphql_entity_for(user_achievement2)
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ user2 = create(:user)
+ create_list(:achievement, 3, namespace: group) do |a|
+ create(:user_achievement, achievement: a, user: user2)
+ end
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ post_graphql(query, current_user: user)
+ end
+
+ specify { expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes)).to be_empty }
+ end
+end
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
index f76bb8ff837..d77e66d2239 100644
--- a/spec/requests/api/graphql/ci/config_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_category: :pipeline_composition do
include GraphqlHelpers
include ReactiveCachingHelpers
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index d78b30787c9..042f93e9779 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index 5b65ae88426..286a7af3c01 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.ciVariables', feature_category: :pipeline_composition do
include GraphqlHelpers
let(:query) do
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 674407c0a0e..3f556a75869 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -260,6 +260,68 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
end
end
+ describe '.jobs.runnerMachine' do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:runner_machine) { create(:ci_runner_machine, created_at: Time.current, contacted_at: Time.current) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) do
+ create(:ci_build, pipeline: pipeline, name: 'my test job', runner_machine: runner_machine)
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ jobs {
+ nodes {
+ id
+ name
+ runnerMachine {
+ #{all_graphql_fields_for('CiRunnerMachine', excluded: [:runner], max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:jobs_graphql_data) { graphql_data_at(:project, :pipeline, :jobs, :nodes) }
+
+ it 'returns the runner machine in each job of a pipeline' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_graphql_data).to contain_exactly(
+ a_graphql_entity_for(
+ build,
+ name: build.name,
+ runner_machine: a_graphql_entity_for(
+ runner_machine,
+ system_id: runner_machine.system_xid,
+ created_at: runner_machine.created_at.iso8601,
+ contacted_at: runner_machine.contacted_at.iso8601,
+ status: runner_machine.status.to_s.upcase
+ )
+ )
+ )
+ end
+
+ it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ admin2 = create(:admin)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: admin)
+ end
+
+ runner_machine2 = create(:ci_runner_machine)
+ create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_machine: runner_machine2)
+
+ expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control)
+ end
+ end
+
describe '.jobs.count' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:successful_job) { create(:ci_build, :success, pipeline: pipeline) }
diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb
index 921c69e535d..98d91e9ded0 100644
--- a/spec/requests/api/graphql/ci/manual_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index 0ddcac89b34..947991a2e62 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipeline_composition do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 986e3ce9e52..da71ee675b7 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -6,11 +6,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:another_admin) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:active_instance_runner) do
- create(:ci_runner, :instance,
+ create(:ci_runner, :instance, :with_runner_machine,
description: 'Runner 1',
+ creator: user,
contacted_at: 2.hours.ago,
active: true,
version: 'adfe156',
@@ -28,6 +30,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:inactive_instance_runner) do
create(:ci_runner, :instance,
description: 'Runner 2',
+ creator: another_admin,
contacted_at: 1.day.ago,
active: false,
version: 'adfe157',
@@ -55,7 +58,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
let_it_be(:project1) { create(:project) }
- let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
+ let_it_be(:active_project_runner) do
+ create(:ci_runner, :project, :with_runner_machine, projects: [project1])
+ end
shared_examples 'runner details fetch' do
let(:query) do
@@ -77,6 +82,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
expect(runner_data).to match a_graphql_entity_for(
runner,
description: runner.description,
+ created_by: runner.creator ? a_graphql_entity_for(runner.creator) : nil,
created_at: runner.created_at&.iso8601,
contacted_at: runner.contacted_at&.iso8601,
version: runner.version,
@@ -107,15 +113,39 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
),
project_count: nil,
admin_url: "http://localhost/admin/runners/#{runner.id}",
+ edit_admin_url: "http://localhost/admin/runners/#{runner.id}/edit",
+ register_admin_url: runner.registration_available? ? "http://localhost/admin/runners/#{runner.id}/register" : nil,
user_permissions: {
'readRunner' => true,
'updateRunner' => true,
'deleteRunner' => true,
'assignRunner' => true
- }
+ },
+ machines: a_hash_including(
+ "count" => runner.runner_machines.count,
+ "nodes" => an_instance_of(Array),
+ "pageInfo" => anything
+ )
)
expect(runner_data['tagList']).to match_array runner.tag_list
end
+
+ it 'does not execute more queries per runner', :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+ expect(graphql_data_at(:runner)).not_to be_nil
+
+ personal_access_token = create(:personal_access_token, user: another_admin)
+ args = { current_user: another_admin, token: { personal_access_token: personal_access_token } }
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) }
+
+ create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: another_admin)
+ create(:ci_runner, :project, version: '14.0.1', projects: [project1], tag_list: %w[tag3 tag8], creator: another_admin)
+
+ expect { post_graphql(query, **args) }.not_to exceed_query_limit(control)
+ end
end
shared_examples 'retrieval with no admin url' do
@@ -135,7 +165,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
runner_data = graphql_data_at(:runner)
expect(runner_data).not_to be_nil
- expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil)
+ expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil, edit_admin_url: nil)
expect(runner_data['tagList']).to match_array runner.tag_list
end
end
@@ -307,6 +337,24 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
it_behaves_like 'runner details fetch'
end
+ describe 'for registration type' do
+ context 'when registered with registration token' do
+ let(:runner) do
+ create(:ci_runner, registration_type: :registration_token)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
+
+ context 'when registered with authenticated user' do
+ let(:runner) do
+ create(:ci_runner, registration_type: :authenticated_user)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
+ end
+
describe 'for group runner request' do
let(:query) do
%(
@@ -568,14 +616,14 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
- context 'with request made by creator' do
+ context 'with request made by creator', :frozen_time do
let(:user) { creator }
context 'with runner created in UI' do
let(:registration_type) { :authenticated_user }
- context 'with runner created in last 3 hours' do
- let(:created_at) { (3.hours - 1.second).ago }
+ context 'with runner created in last hour' do
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
context 'with no runner machine registed yet' do
it_behaves_like 'an ephemeral_authentication_token'
@@ -589,13 +637,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
context 'with runner created almost too long ago' do
- let(:created_at) { (3.hours - 1.second).ago }
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
it_behaves_like 'an ephemeral_authentication_token'
end
context 'with runner created too long ago' do
- let(:created_at) { 3.hours.ago }
+ let(:created_at) { Ci::Runner::REGISTRATION_AVAILABILITY_TIME.ago }
it_behaves_like 'a protected ephemeral_authentication_token'
end
@@ -604,8 +652,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
context 'with runner registered from command line' do
let(:registration_type) { :registration_token }
- context 'with runner created in last 3 hours' do
- let(:created_at) { (3.hours - 1.second).ago }
+ context 'with runner created in last 1 hour' do
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
it_behaves_like 'a protected ephemeral_authentication_token'
end
@@ -628,6 +676,12 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
<<~SINGLE
runner(id: "#{runner.to_global_id}") {
#{all_graphql_fields_for('CiRunner', excluded: excluded_fields)}
+ createdBy {
+ id
+ username
+ webPath
+ webUrl
+ }
groups {
nodes {
id
@@ -658,7 +712,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:active_group_runner2) { create(:ci_runner, :group) }
# Exclude fields that are already hardcoded above
- let(:excluded_fields) { %w[jobs groups projects ownerProject] }
+ let(:excluded_fields) { %w[createdBy jobs groups projects ownerProject] }
let(:single_query) do
<<~QUERY
@@ -691,6 +745,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) }
+ personal_access_token = create(:personal_access_token, user: another_admin)
+ args = { current_user: another_admin, token: { personal_access_token: personal_access_token } }
expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control)
expect(graphql_data.count).to eq 6
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 75d8609dc38..c8706ae9698 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -11,16 +11,24 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: 'abc', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, active: false, version: 'def', revision: '456', description: 'Project runner', projects: [project], ip_address: '127.0.0.1') }
- let(:runners_graphql_data) { graphql_data['runners'] }
+ let(:runners_graphql_data) { graphql_data_at(:runners) }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('CiRunner', excluded: %w[ownerProject])}
+ #{all_graphql_fields_for('CiRunner', excluded: %w[createdBy ownerProject])}
+ createdBy {
+ username
+ webPath
+ webUrl
+ }
ownerProject {
id
+ path
+ fullPath
+ webUrl
}
}
QUERY
@@ -50,6 +58,25 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
it 'returns expected runner' do
expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner))
end
+
+ it 'does not execute more queries per runner', :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: current_user)
+ args = { current_user: current_user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+ expect(graphql_data_at(:runners, :nodes)).not_to be_empty
+
+ admin2 = create(:admin)
+ personal_access_token = create(:personal_access_token, user: admin2)
+ args = { current_user: admin2, token: { personal_access_token: personal_access_token } }
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) }
+
+ create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2)
+ create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8],
+ creator: current_user)
+
+ expect { post_graphql(query, **args) }.not_to exceed_query_limit(control)
+ end
end
context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb
index f7e23aeb241..ee019a99f8d 100644
--- a/spec/requests/api/graphql/current_user/todos_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Query current user todos', feature_category: :source_code_manage
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('todos'.classify, max_depth: 2)}
+ #{all_graphql_fields_for('todos'.classify, max_depth: 2, excluded: ['productAnalyticsState'])}
}
QUERY
end
diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb
index 53d2580caee..aceef77920d 100644
--- a/spec/requests/api/graphql/current_user_query_spec.rb
+++ b/spec/requests/api/graphql/current_user_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project information', feature_category: :authentication_and_authorization do
+RSpec.describe 'getting project information', feature_category: :system_access do
include GraphqlHelpers
let(:fields) do
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index 7b804623e01..1858ea831dd 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting custom emoji within namespace', feature_category: :not_owned do
+RSpec.describe 'getting custom emoji within namespace', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb
index 4d615d3eaa4..0a5c87ebef8 100644
--- a/spec/requests/api/graphql/multiplexed_queries_spec.rb
+++ b/spec/requests/api/graphql/multiplexed_queries_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Multiplexed queries', feature_category: :not_owned do
+RSpec.describe 'Multiplexed queries', feature_category: :shared do
include GraphqlHelpers
it 'returns responses for multiple queries' do
diff --git a/spec/requests/api/graphql/mutations/achievements/award_spec.rb b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
new file mode 100644
index 00000000000..9bc0751e924
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:recipient) { create(:user) }
+
+ let(:mutation) { graphql_mutation(:achievements_award, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:recipient_id) { recipient&.to_global_id }
+ let(:params) do
+ {
+ achievement_id: achievement_id,
+ user_id: recipient_id
+ }
+ end
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create an achievement' do
+ expect { subject }.not_to change { Achievements::UserAchievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the recipient_id is invalid' do
+ let(:recipient_id) { "gid://gitlab/User/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_data_at(:achievements_award,
+ :errors)).to include("Couldn't find User with 'id'=#{non_existing_record_id}")
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'creates an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.count }.by(1)
+ end
+
+ it 'returns the new achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_award, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_award, :user_achievement, :user, :id))
+ .to eq(recipient.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb
new file mode 100644
index 00000000000..925a1bb9fcc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Revoke, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ let(:mutation) { graphql_mutation(:achievements_revoke, params) }
+ let(:user_achievement_id) { user_achievement&.to_global_id }
+ let(:params) { { user_achievement_id: user_achievement_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not revoke any achievements' do
+ expect { subject }.not_to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:user_achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for userAchievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the user_achievement_id is invalid' do
+ let(:user_achievement_id) { "gid://gitlab/Achievements::UserAchievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'revokes an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }.by(-1)
+ end
+
+ it 'returns the revoked achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_by_user, :id))
+ .to eq(current_user.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_at))
+ .not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
index 64ea6d32f5f..b3d25155a6f 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :not_owned do
+RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :shared do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index fdbff0f93cd..18cc85d36e0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Adding an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
index e200bfc2d18..7ec2b061a88 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Removing an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Removing an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 6dba2b58357..7c6a487cdd0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Toggling an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb
index 468a9e57f56..abad1ae0812 100644
--- a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb
@@ -15,12 +15,12 @@ RSpec.describe "JobCancel", feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_cancel, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb
index 9ba80e51dee..8100274ed97 100644
--- a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
let(:mutation) do
graphql_mutation(:job_play, variables,
- <<-QL
+ <<-QL
errors
job {
id
@@ -28,7 +28,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
}
}
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb
index e49ee6f3163..4114c77491b 100644
--- a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb
@@ -16,12 +16,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_retry, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
@@ -57,12 +57,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do
}
graphql_mutation(:job_retry, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb
index 6868b0ea279..08e155e808b 100644
--- a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'JobUnschedule', feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_unschedule, variables,
- <<-QL
+ <<-QL
errors
job {
id
diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb
new file mode 100644
index 00000000000..4e25669a0ca
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'BulkDestroy', feature_category: :build_artifacts do
+ include GraphqlHelpers
+
+ let(:maintainer) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:first_artifact) { create(:ci_job_artifact) }
+ let(:second_artifact) { create(:ci_job_artifact, project: project) }
+ let(:second_artifact_another_project) { create(:ci_job_artifact) }
+ let(:project) { first_artifact.job.project }
+ let(:ids) { [first_artifact.to_global_id.to_s] }
+ let(:not_authorized_project_error_message) do
+ "The resource that you are attempting to access " \
+ "does not exist or you don't have permission to perform this action"
+ end
+
+ let(:mutation) do
+ variables = {
+ project_id: project.to_global_id.to_s,
+ ids: ids
+ }
+ graphql_mutation(:bulk_destroy_job_artifacts, variables, <<~FIELDS)
+ destroyedCount
+ destroyedIds
+ errors
+ FIELDS
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:bulk_destroy_job_artifacts) }
+
+ it 'fails to destroy the artifact if a user not in a project' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => not_authorized_project_error_message)
+ )
+
+ expect(first_artifact.reload).to be_persisted
+ end
+
+ context 'when the `ci_job_artifact_bulk_destroy` feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_job_artifact_bulk_destroy: false)
+ project.add_maintainer(maintainer)
+ end
+
+ it 'returns a resource not available error' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => '`ci_job_artifact_bulk_destroy` feature flag is disabled.'
+ )
+ )
+ end
+ end
+
+ context "when the user is a developer in a project" do
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'fails to destroy the artifact' do
+ post_graphql_mutation(mutation, current_user: developer)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => not_authorized_project_error_message)
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(first_artifact.reload).to be_persisted
+ end
+ end
+
+ context "when the user is a maintainer in a project" do
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ shared_examples 'failing mutation' do
+ it 'rejects the request' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors(mutation_response)).to include(expected_error_message)
+
+ expected_not_found_artifacts.each do |artifact|
+ expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ expected_found_artifacts.each do |artifact|
+ expect(artifact.reload).to be_persisted
+ end
+ end
+ end
+
+ it 'destroys the artifact' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(mutation_response).to include("destroyedCount" => 1, "destroyedIds" => [gid_string(first_artifact)])
+ expect(response).to have_gitlab_http_status(:success)
+ expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context "and one artifact doesn't belong to the project" do
+ let(:not_owned_artifact) { create(:ci_job_artifact) }
+ let(:ids) { [first_artifact.to_global_id.to_s, not_owned_artifact.to_global_id.to_s] }
+ let(:expected_error_message) { "Not all artifacts belong to requested project" }
+ let(:expected_not_found_artifacts) { [] }
+ let(:expected_found_artifacts) { [first_artifact, not_owned_artifact] }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "and multiple artifacts belong to the maintainer's project" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] }
+
+ it 'destroys all artifacts' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(mutation_response).to include(
+ "destroyedCount" => 2,
+ "destroyedIds" => [gid_string(first_artifact), gid_string(second_artifact)]
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { second_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context "and one artifact belongs to a different maintainer's project" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact_another_project.to_global_id.to_s] }
+ let(:expected_found_artifacts) { [first_artifact, second_artifact_another_project] }
+ let(:expected_not_found_artifacts) { [] }
+ let(:expected_error_message) { "Not all artifacts belong to requested project" }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "and not found" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] }
+ let(:not_found_ids) { expected_not_found_artifacts.map(&:id).join(',') }
+ let(:expected_error_message) { "Artifacts (#{not_found_ids}) not found" }
+
+ before do
+ expected_not_found_artifacts.each(&:destroy!)
+ end
+
+ context "with one artifact" do
+ let(:expected_not_found_artifacts) { [second_artifact] }
+ let(:expected_found_artifacts) { [first_artifact] }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "with all artifact" do
+ let(:expected_not_found_artifacts) { [first_artifact, second_artifact] }
+ let(:expected_found_artifacts) { [] }
+
+ it_behaves_like 'failing mutation'
+ end
+ end
+
+ context 'when empty request' do
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ context 'with nil value' do
+ let(:ids) { nil }
+
+ it 'does nothing and returns empty answer' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect_graphql_errors_to_include(/was provided invalid value for ids \(Expected value to not be null\)/)
+ end
+ end
+
+ context 'with empty array' do
+ let(:ids) { [] }
+
+ it 'raises argument error' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect_graphql_errors_to_include(/IDs array of job artifacts can not be empty/)
+ end
+ end
+ end
+
+ def gid_string(object)
+ Gitlab::GlobalId.build(object, id: object.id).to_s
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 99e55c44773..0951d165d46 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -101,21 +101,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(response).to have_gitlab_http_status(:success)
expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
end
-
- context 'when ci_inbound_job_token_scope disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- it 'does not update inbound_job_token_scope_enabled' do
- post_graphql_mutation(mutation, current_user: user)
-
- project.reload
-
- expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
- end
- end
end
it 'updates ci_opt_in_jwt' do
diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
new file mode 100644
index 00000000000..f39f6f84c99
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+
+ let(:mutation_params) do
+ {
+ description: 'create description',
+ maintenance_note: 'create maintenance note',
+ maximum_timeout: 900,
+ access_level: 'REF_PROTECTED',
+ paused: true,
+ run_untagged: false,
+ tag_list: %w[tag1 tag2]
+ }
+ end
+
+ let(:mutation) do
+ variables = {
+ **mutation_params
+ }
+
+ graphql_mutation(
+ :runner_create,
+ variables,
+ <<-QL
+ runner {
+ ephemeralAuthenticationToken
+
+ runnerType
+ description
+ maintenanceNote
+ paused
+ tagList
+ accessLevel
+ locked
+ maximumTimeout
+ runUntagged
+ }
+ errors
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:runner_create) }
+
+ context 'when user does not have permissions' do
+ let(:current_user) { user }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['errors']).to contain_exactly "Insufficient permissions"
+ end
+ end
+
+ context 'when user has permissions', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ context 'when :create_runner_workflow_for_admin feature flag is disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow_for_admin: false)
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).not_to be_empty
+ expect(graphql_errors[0]['message'])
+ .to eq("`create_runner_workflow_for_admin` feature flag is disabled.")
+ end
+ end
+
+ context 'when success' do
+ it do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ mutation_params.each_key do |key|
+ expect(mutation_response['runner'][key.to_s.camelize(:lower)]).to eq mutation_params[key]
+ end
+
+ expect(mutation_response['runner']['ephemeralAuthenticationToken']).to start_with 'glrt'
+
+ expect(mutation_response['errors']).to eq([])
+ end
+ end
+
+ context 'when failure' do
+ let(:mutation_params) do
+ {
+ description: "",
+ maintenanceNote: "",
+ paused: true,
+ accessLevel: "NOT_PROTECTED",
+ runUntagged: false,
+ tagList:
+ [],
+ maximumTimeout: 1
+ }
+ end
+
+ it do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors']).to contain_exactly(
+ "Tags list can not be empty when runner is not allowed to pick untagged jobs",
+ "Maximum timeout needs to be at least 10 minutes"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
index ea2ce8a13e2..19a52086f34 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new Custom Emoji', feature_category: :not_owned do
+RSpec.describe 'Creation of a new Custom Emoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
index ad7a043909a..2623d3d8410 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deletion of custom emoji', feature_category: :not_owned do
+RSpec.describe 'Deletion of custom emoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/mutations/design_management/update_spec.rb b/spec/requests/api/graphql/mutations/design_management/update_spec.rb
new file mode 100644
index 00000000000..9558f2538f1
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/design_management/update_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "updating designs", feature_category: :design_management do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be_with_reload(:design) { create(:design, description: 'old description', issue: issue) }
+ let_it_be(:developer) { create(:user, developer_projects: [issue.project]) }
+
+ let(:user) { developer }
+ let(:description) { 'new description' }
+
+ let(:mutation) do
+ input = {
+ id: design.to_global_id.to_s,
+ description: description
+ }.compact
+
+ graphql_mutation(:design_management_update, input, <<~FIELDS)
+ errors
+ design {
+ description
+ descriptionHtml
+ }
+ FIELDS
+ end
+
+ let(:update_design) { post_graphql_mutation(mutation, current_user: user) }
+ let(:mutation_response) { graphql_mutation_response(:design_management_update) }
+
+ before do
+ enable_design_management
+ end
+
+ it 'updates design' do
+ update_design
+
+ expect(graphql_errors).not_to be_present
+ expect(mutation_response).to eq(
+ 'errors' => [],
+ 'design' => {
+ 'description' => description,
+ 'descriptionHtml' => "<p data-sourcepos=\"1:1-1:15\" dir=\"auto\">#{description}</p>"
+ }
+ )
+ end
+
+ context 'when the user is not allowed to update designs' do
+ let(:user) { create(:user) }
+
+ it 'returns an error' do
+ update_design
+
+ expect(graphql_errors).to be_present
+ end
+ end
+
+ context 'when update fails' do
+ let(:description) { 'x' * 1_000_001 }
+
+ it 'returns an error' do
+ update_design
+
+ expect(graphql_errors).not_to be_present
+ expect(mutation_response).to eq(
+ 'errors' => ["Description is too long (maximum is 1000000 characters)"],
+ 'design' => {
+ 'description' => 'old description',
+ 'descriptionHtml' => '<p data-sourcepos="1:1-1:15" dir="auto">old description</p>'
+ }
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
index b9c83311908..b729585a89b 100644
--- a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
@@ -8,7 +8,9 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group).tap { |group| group.add_developer(developer) } }
let_it_be(:project) { create(:project, group: group) }
- let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project) }
+ let_it_be(:label1) { create(:group_label, group: group) }
+ let_it_be(:label2) { create(:group_label, group: group) }
+ let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) }
let_it_be(:milestone) { create(:milestone, group: group) }
let(:parent) { project }
@@ -21,10 +23,36 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let(:additional_arguments) do
{
assignee_ids: [current_user.to_gid.to_s],
- milestone_id: milestone.to_gid.to_s
+ milestone_id: milestone.to_gid.to_s,
+ state_event: :CLOSE,
+ add_label_ids: [label2.to_gid.to_s],
+ remove_label_ids: [label1.to_gid.to_s],
+ subscription_event: :UNSUBSCRIBE
}
end
+ before_all do
+ updatable_issues.each { |i| i.subscribe(developer, project) }
+ end
+
+ context 'when Gitlab is FOSS only' do
+ unless Gitlab.ee?
+ context 'when parent is a group' do
+ let(:parent) { group }
+
+ it 'does not allow bulk updating issues at the group level' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => match(/does not represent an instance of IssueParent/)
+ )
+ )
+ end
+ end
+ end
+ end
+
context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
before do
stub_feature_flags(bulk_update_issues_mutation: false)
@@ -67,6 +95,11 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
updatable_issues.each(&:reload)
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
.and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
+ .and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2))
+ .and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2))
+ .and(
+ change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2)
+ )
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
@@ -88,37 +121,6 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
end
end
- context 'when scoping to a parent group' do
- let(:parent) { group }
-
- it 'updates all issues' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- updatable_issues.each(&:reload)
- end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
- .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
-
- expect(mutation_response).to include(
- 'updatedIssueCount' => updatable_issues.count
- )
- end
-
- context 'when current user cannot read the specified group' do
- let(:parent) { create(:group, :private) }
-
- it 'returns a resource not found error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(graphql_errors).to contain_exactly(
- hash_including(
- 'message' => "The resource that you are attempting to access does not exist or you don't have " \
- 'permission to perform this action'
- )
- )
- end
- end
- end
-
context 'when setting arguments to null or none' do
let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
diff --git a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
index ad70129a7bc..f15b52f53a3 100644
--- a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
@@ -5,126 +5,14 @@ require 'spec_helper'
RSpec.describe 'GroupMemberBulkUpdate', feature_category: :subgroups do
include GraphqlHelpers
- let_it_be(:current_user) { create(:user) }
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:group_member1) { create(:group_member, group: group, user: user1) }
- let_it_be(:group_member2) { create(:group_member, group: group, user: user2) }
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
+ let_it_be(:group) { create(:group, parent: parent_group) }
+ let_it_be(:source) { group }
+ let_it_be(:member_type) { :group_member }
let_it_be(:mutation_name) { :group_member_bulk_update }
+ let_it_be(:source_id_key) { 'group_id' }
+ let_it_be(:response_member_field) { 'groupMembers' }
- let(:input) do
- {
- 'group_id' => group.to_global_id.to_s,
- 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
- 'access_level' => 'GUEST'
- }
- end
-
- let(:extra_params) { { expires_at: 10.days.from_now } }
- let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(mutation_name, input_params) }
- let(:mutation_response) { graphql_mutation_response(mutation_name) }
-
- context 'when user is not logged-in' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user is not an owner' do
- before do
- group.add_maintainer(current_user)
- end
-
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user is an owner' do
- before do
- group.add_owner(current_user)
- end
-
- shared_examples 'updates the user access role' do
- specify do
- post_graphql_mutation(mutation, current_user: current_user)
-
- new_access_levels = mutation_response['groupMembers'].map { |member| member['accessLevel']['integerValue'] }
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['errors']).to be_empty
- expect(new_access_levels).to all(be Gitlab::Access::GUEST)
- end
- end
-
- it_behaves_like 'updates the user access role'
-
- context 'when inherited members are passed' do
- let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:subgroup_member) { create(:group_member, group: subgroup) }
-
- let(:input) do
- {
- 'group_id' => group.to_global_id.to_s,
- 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, subgroup_member.user.to_global_id.to_s],
- 'access_level' => 'GUEST'
- }
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- error = Mutations::Members::Groups::BulkUpdate::INVALID_MEMBERS_ERROR
- expect(json_response['errors'].first['message']).to include(error)
- end
- end
-
- context 'when members count is more than the allowed limit' do
- let(:max_members_update_limit) { 1 }
-
- before do
- stub_const('Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- error = Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_ERROR
- expect(json_response['errors'].first['message']).to include(error)
- end
- end
-
- context 'when the update service raises access denied error' do
- before do
- allow_next_instance_of(Members::UpdateService) do |instance|
- allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
- end
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['groupMembers']).to be_nil
- expect(mutation_response['errors'])
- .to contain_exactly("Unable to update members, please check user permissions.")
- end
- end
-
- context 'when the update service returns an error message' do
- before do
- allow_next_instance_of(Members::UpdateService) do |instance|
- error_result = {
- message: 'Expires at cannot be a date in the past',
- status: :error,
- members: [group_member1]
- }
- allow(instance).to receive(:execute).and_return(error_result)
- end
- end
-
- it 'will pass through the error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['groupMembers'].first['id']).to eq(group_member1.to_global_id.to_s)
- expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
- end
- end
- end
+ it_behaves_like 'members bulk update mutation'
end
diff --git a/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb
new file mode 100644
index 00000000000..cbef9715cbe
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :projects do
+ include GraphqlHelpers
+
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
+ let_it_be(:project) { create(:project, group: parent_group) }
+ let_it_be(:source) { project }
+ let_it_be(:member_type) { :project_member }
+ let_it_be(:mutation_name) { :project_member_bulk_update }
+ let_it_be(:source_id_key) { 'project_id' }
+ let_it_be(:response_member_field) { 'projectMembers' }
+
+ it_behaves_like 'members bulk update mutation'
+end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index bce57b47aab..3c7f4a030f9 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ
graphql_mutation_response(:create_annotation)
end
- specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) }
context 'when annotation source is environment' do
let(:mutation) do
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index f505dc25dc0..c104138b725 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
graphql_mutation_response(:delete_annotation)
end
- specify { expect(described_class).to require_graphql_authorizations(:delete_metrics_dashboard_annotation) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) }
context 'when the user has permission to delete the annotation' do
before do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index a6253ba424b..2a0b5f291dc 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -104,7 +104,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'as work item' do
- let(:noteable) { create(:work_item, :issue, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:noteable) { create(:work_item, :issue, project: project) }
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -130,6 +131,19 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
+
+ context 'when body contains quick actions' do
+ let_it_be(:noteable) { create(:work_item, :task, project: project) }
+
+ let(:variables_extra) { {} }
+
+ it_behaves_like 'work item supports labels widget updates via quick actions'
+ it_behaves_like 'work item does not support labels widget updates via quick actions'
+ it_behaves_like 'work item supports assignee widget updates via quick actions'
+ it_behaves_like 'work item does not support assignee widget updates via quick actions'
+ it_behaves_like 'work item supports start and due date widget updates via quick actions'
+ it_behaves_like 'work item does not support start and due date widget updates via quick actions'
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb
new file mode 100644
index 00000000000..a77c026dd06
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Sync project fork", feature_category: :source_code_management do
+ include GraphqlHelpers
+ include ProjectForksHelper
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:source_project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [source_project]) }
+ let_it_be(:project, refind: true) { fork_project(source_project, current_user, { repository: true }) }
+ let_it_be(:target_branch) { project.default_branch }
+
+ let(:mutation) do
+ params = { project_path: project.full_path, target_branch: target_branch }
+
+ graphql_mutation(:project_sync_fork, params) do
+ <<-QL.strip_heredoc
+ details {
+ ahead
+ behind
+ isSyncing
+ hasConflicts
+ }
+ errors
+ QL
+ end
+ end
+
+ before do
+ source_project.change_head('feature')
+ end
+
+ context 'when synchronize_fork feature flag is disabled' do
+ before do
+ stub_feature_flags(synchronize_fork: false)
+ end
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)).to eq(
+ {
+ 'details' => nil,
+ 'errors' => ['Feature flag is disabled']
+ })
+ end
+ end
+
+ context 'when the user does not have permission' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when the user has permission' do
+ context 'and the sync service executes successfully', :sidekiq_inline do
+ it 'calls the sync service' do
+ expect(::Projects::Forks::SyncWorker).to receive(:perform_async).and_call_original
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)).to eq(
+ {
+ 'details' => { 'ahead' => 30, 'behind' => 0, "hasConflicts" => false, "isSyncing" => false },
+ 'errors' => []
+ })
+ end
+ end
+
+ context 'and the sync service fails to execute' do
+ let(:target_branch) { 'markdown' }
+
+ def expect_error_response(message)
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)['errors']).to eq([message])
+ end
+
+ context 'when fork details cannot be resolved' do
+ let_it_be(:project) { source_project }
+
+ it 'returns an error' do
+ expect_error_response('This branch of this project cannot be updated from the upstream')
+ end
+ end
+
+ context 'when the previous execution resulted in a conflict' do
+ it 'returns an error' do
+ expect_next_instance_of(::Projects::Forks::Details) do |instance|
+ expect(instance).to receive(:has_conflicts?).twice.and_return(true)
+ end
+
+ expect_error_response('The synchronization cannot happen due to the merge conflict')
+ expect(graphql_mutation_response(:project_sync_fork)['details']['hasConflicts']).to eq(true)
+ end
+ end
+
+ context 'when the request is rate limited' do
+ it 'returns an error' do
+ expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+
+ expect_error_response('This service has been called too many times.')
+ end
+ end
+
+ context 'when another fork sync is in progress' do
+ it 'returns an error' do
+ expect_next_instance_of(Projects::Forks::Details) do |instance|
+ lease = instance_double(Gitlab::ExclusiveLease, try_obtain: false, exists?: true)
+ expect(instance).to receive(:exclusive_lease).twice.and_return(lease)
+ end
+
+ expect_error_response('Another fork sync is already in progress')
+ expect(graphql_mutation_response(:project_sync_fork)['details']['isSyncing']).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index fa087e6773c..3b98ee3c2e9 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -193,7 +193,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:user) { current_user }
let(:property) { 'g_edit_by_snippet_ide' }
let(:namespace) { project.namespace }
@@ -203,8 +202,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let(:context) do
[Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context]
end
-
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
index 97bf060356a..d2df3e8bda2 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe "Create a work item from a task in a work item's description", fe
}
end
- let(:mutation) { graphql_mutation(:workItemCreateFromTask, input) }
+ let(:mutation) { graphql_mutation(:workItemCreateFromTask, input, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:work_item_create_from_task) }
context 'the user is not allowed to update a work item' do
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index 16f78b67b5c..7519389ab49 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s
}
end
@@ -43,14 +43,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(mutation_response['workItem']).to include(
input.except('workItemTypeId').merge(
- 'id' => created_work_item.to_global_id.to_s,
+ 'id' => created_work_item.to_gid.to_s,
'workItemType' => hash_including('name' => 'Task')
)
)
end
context 'when input is invalid' do
- let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } }
+ let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s } }
it 'does not create and returns validation errors' do
expect do
@@ -98,8 +98,8 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
let(:input) do
{
title: 'item1',
- workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
- hierarchyWidget: { 'parentId' => parent.to_global_id.to_s }
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_gid.to_s }
}
end
@@ -110,7 +110,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(widgets_response).to include(
{
'children' => { 'edges' => [] },
- 'parent' => { 'id' => parent.to_global_id.to_s },
+ 'parent' => { 'id' => parent.to_gid.to_s },
'type' => 'HIERARCHY'
}
)
@@ -137,6 +137,40 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(graphql_errors.first['message']).to include('No object found for `parentId')
end
end
+
+ context 'when adjacent is already in place' do
+ let_it_be(:adjacent) { create(:work_item, :task, project: project) }
+
+ let(:work_item) { WorkItem.last }
+
+ let(:input) do
+ {
+ title: 'item1',
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_gid.to_s }
+ }
+ end
+
+ before(:all) do
+ create(:parent_link, work_item_parent: parent, work_item: adjacent, relative_position: 0)
+ end
+
+ it 'creates work item and sets the relative position to be AFTER adjacent' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => parent.to_gid.to_s },
+ 'type' => 'HIERARCHY'
+ }
+ )
+ expect(work_item.parent_link.relative_position).to be > adjacent.parent_link.relative_position
+ end
+ end
end
context 'when unsupported widget input is sent' do
@@ -144,7 +178,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
{
'title' => 'new title',
'description' => 'new description',
- 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s,
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_gid.to_s,
'hierarchyWidget' => {}
}
end
@@ -181,8 +215,8 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
let(:input) do
{
title: 'some WI',
- workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
- milestoneWidget: { 'milestoneId' => milestone.to_global_id.to_s }
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ milestoneWidget: { 'milestoneId' => milestone.to_gid.to_s }
}
end
@@ -196,7 +230,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(widgets_response).to include(
{
'type' => 'MILESTONE',
- 'milestone' => { 'id' => milestone.to_global_id.to_s }
+ 'milestone' => { 'id' => milestone.to_gid.to_s }
}
)
end
diff --git a/spec/requests/api/graphql/mutations/work_items/export_spec.rb b/spec/requests/api/graphql/mutations/work_items/export_spec.rb
new file mode 100644
index 00000000000..3cadaab5201
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/export_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Export work items', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
+ let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+
+ let(:input) { { 'projectPath' => project.full_path } }
+ let(:mutation) { graphql_mutation(:workItemExport, input) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_export) }
+
+ context 'when user is not allowed to export work items' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when import_export_work_items_csv feature flag is disabled' do
+ let(:current_user) { reporter }
+
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['`import_export_work_items_csv` feature flag is disabled.']
+ end
+
+ context 'when user has permissions to export work items' do
+ let(:current_user) { reporter }
+ let(:input) do
+ super().merge(
+ 'selectedFields' => %w[TITLE AUTHOR TYPE AUTHOR_USERNAME CREATED_AT],
+ 'authorUsername' => 'admin',
+ 'iids' => [work_item.iid.to_s],
+ 'state' => 'opened',
+ 'types' => 'TASK',
+ 'search' => 'any',
+ 'in' => 'TITLE'
+ )
+ end
+
+ it 'schedules export job with given arguments', :aggregate_failures do
+ expected_arguments = {
+ selected_fields: ['title', 'author', 'type', 'author username', 'created_at'],
+ author_username: 'admin',
+ iids: [work_item.iid.to_s],
+ state: 'opened',
+ issue_types: ['task'],
+ search: 'any',
+ in: ['title']
+ }
+
+ expect(IssuableExportCsvWorker)
+ .to receive(:perform_async).with(:work_item, current_user.id, project.id, expected_arguments)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['errors']).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index ddd294e8f82..76dc60be799 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -7,10 +7,11 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:author) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
- let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
+ let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: author) }
let(:work_item_event) { 'CLOSE' }
let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } }
@@ -843,6 +844,140 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
end
+ context 'when updating notifications subscription' do
+ let_it_be(:current_user) { reporter }
+ let(:input) { { 'notificationsWidget' => { 'subscribed' => desired_state } } }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'subscription updated successfully' do
+ let_it_be(:subscription) do
+ create(
+ :subscription, project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: !desired_state
+ )
+ end
+
+ it "updates existing work item's subscription state" do
+ expect do
+ update_work_item
+ subscription.reload
+ end.to change(subscription, :subscribed).to(desired_state)
+ .and(change { work_item.reload.subscribed?(reporter, project) }.to(desired_state))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'subscribed' => desired_state,
+ 'type' => 'NOTIFICATIONS'
+ }
+ )
+ end
+ end
+
+ shared_examples 'subscription update ignored' do
+ context 'when user is subscribed with a subscription record' do
+ let_it_be(:subscription) do
+ create(
+ :subscription, project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: !desired_state
+ )
+ end
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ subscription.reload
+ end.to not_change(subscription, :subscribed)
+ .and(not_change { work_item.subscribed?(current_user, project) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ end.to not_change(Subscription, :count)
+ .and(not_change { work_item.subscribed?(current_user, project) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when work item update fails' do
+ let_it_be(:desired_state) { false }
+ let(:input) { { 'title' => nil, 'notificationsWidget' => { 'subscribed' => desired_state } } }
+
+ it_behaves_like 'subscription update ignored'
+ end
+
+ context 'when user cannot update work item' do
+ let_it_be(:desired_state) { false }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :update_subscription, work_item).and_return(false)
+ end
+
+ it_behaves_like 'subscription update ignored'
+ end
+
+ context 'when user can update work item' do
+ context 'when subscribing to notifications' do
+ let_it_be(:desired_state) { true }
+
+ it_behaves_like 'subscription updated successfully'
+ end
+
+ context 'when unsubscribing from notifications' do
+ let_it_be(:desired_state) { false }
+
+ it_behaves_like 'subscription updated successfully'
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'creates a subscription with desired state' do
+ expect { update_work_item }.to change(Subscription, :count).by(1)
+ .and(change { work_item.reload.subscribed?(author, project) }.to(desired_state))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'subscribed' => desired_state,
+ 'type' => 'NOTIFICATIONS'
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+
context 'when unsupported widget input is sent' do
let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
index 999c685ac6a..717de983871 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Update a work item task', feature_category: :team_planning do
let(:task_params) { { 'title' => 'UPDATED' } }
let(:task_input) { { 'id' => task.to_global_id.to_s }.merge(task_params) }
let(:input) { { 'id' => work_item.to_global_id.to_s, 'taskData' => task_input } }
- let(:mutation) { graphql_mutation(:workItemUpdateTask, input) }
+ let(:mutation) { graphql_mutation(:workItemUpdateTask, input, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:work_item_update_task) }
context 'the user is not allowed to read a work item' do
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 4e12da3e3ab..83edacaf831 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'getting projects', feature_category: :projects do
projects(includeSubgroups: #{include_subgroups}) {
edges {
node {
- #{all_graphql_fields_for('Project', max_depth: 1)}
+ #{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])}
}
}
}
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 82fcc5254ad..7610a4aaac1 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -270,6 +270,31 @@ RSpec.describe 'package details', feature_category: :package_registry do
it 'returns composer_config_repository_url correctly' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ subject
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
+ end
+ end
+
+ context 'when project is public' do
+ let_it_be(:public_project) { create(:project, :public, group: group) }
+ let_it_be(:composer_package) { create(:composer_package, project: public_project) }
+ let(:package_global_id) { global_id_of(composer_package) }
+
+ before do
+ subject
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://localhost/api/v4/projects/#{public_project.id}/packages/pypi/simple")
+ end
+ end
end
context 'web_path' do
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index 7b1b95eaf58..b27cddea07b 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira service', feature_category: :authentication_and_authorization do
+RSpec.describe 'query Jira service', feature_category: :system_access do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 7ccf8a6f5bf..9a40a972256 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:excluded_fields) { %w[pipeline jobs] }
+ let(:excluded_fields) { %w[pipeline jobs productAnalyticsState] }
let(:container_repositories_fields) do
<<~GQL
edges {
@@ -155,7 +155,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
it_behaves_like 'handling graphql network errors with the container registry'
it_behaves_like 'not hitting graphql network errors with the container registry' do
- let(:excluded_fields) { %w[pipeline jobs tags tagsCount] }
+ let(:excluded_fields) { %w[pipeline jobs tags tagsCount productAnalyticsState] }
end
it 'returns the total count of container repositories' do
diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb
new file mode 100644
index 00000000000..3b5758b3a2e
--- /dev/null
+++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project flow metrics', feature_category: :value_stream_management do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project1) { create(:project, :repository, group: group) }
+ # This is done so we can use the same count expectations in the shared examples and
+ # reuse the shared example for the group-level test.
+ let_it_be(:project2) { project1 }
+ let_it_be(:production_environment1) { create(:environment, :production, project: project1) }
+ let_it_be(:production_environment2) { production_environment1 }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) }
+
+ let(:full_path) { project1.full_path }
+ let(:context) { :project }
+
+ it_behaves_like 'value stream analytics flow metrics issueCount examples'
+
+ it_behaves_like 'value stream analytics flow metrics deploymentCount examples'
+end
diff --git a/spec/requests/api/graphql/project/fork_details_spec.rb b/spec/requests/api/graphql/project/fork_details_spec.rb
index efd48b00833..0baf29b970e 100644
--- a/spec/requests/api/graphql/project/fork_details_spec.rb
+++ b/spec/requests/api/graphql/project/fork_details_spec.rb
@@ -10,12 +10,13 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
let_it_be(:forked_project) { fork_project(project, current_user, repository: true) }
+ let(:ref) { 'feature' }
let(:queried_project) { forked_project }
let(:query) do
graphql_query_for(:project,
{ full_path: queried_project.full_path }, <<~QUERY
- forkDetails(ref: "feature"){
+ forkDetails(ref: "#{ref}"){
ahead
behind
}
@@ -41,6 +42,38 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
end
end
+ context 'when project source is not visible' do
+ it 'does not return fork details' do
+ project.team.truncate
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+
+ context 'when the specified ref does not exist' do
+ let(:ref) { 'non-existent-branch' }
+
+ it 'does not return fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+
+ context 'when fork_divergence_counts feature flag is disabled' do
+ before do
+ stub_feature_flags(fork_divergence_counts: false)
+ end
+
+ it 'does not return fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+
context 'when a user cannot read the code' do
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 8407faa967e..156886ca211 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -588,8 +588,9 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
let(:query) do
+ # Adding a no-op `not` filter to mimic the same query as the frontend does
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
- mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0) {
+ mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0, not: { labels: null }) {
totalTimeToMerge
count
}
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index f49165a88ea..d5dd12de63e 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -313,6 +313,34 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
end
+ context 'when fetching work item notifications widget' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create_list(:work_item, 3, project: project)
+
+ # Performs 1 extra query per item to fetch subscriptions
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control).with_threshold(3)
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
def item_ids
graphql_dig_at(items_data, :node, :id)
end
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index 2b9d66ec744..d93077b1c70 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query', feature_category: :not_owned do
+RSpec.describe 'Query', feature_category: :shared do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..bf9b2b429cc
--- /dev/null
+++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievements) { create_list(:user_achievement, 2, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('user', { id: user.to_global_id.to_s }, fields)
+ end
+
+ let(:current_user) { user }
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all user_achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievements[0]),
+ a_graphql_entity_for(user_achievements[1])
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ achievement2 = create(:achievement, namespace: group)
+ create_list(:user_achievement, 2, achievement: achievement2, user: user)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled for a namespace' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:achievement2) { create(:achievement, namespace: group2) }
+ let_it_be(:user_achievement2) { create(:user_achievement, achievement: achievement2, user: user) }
+
+ before do
+ stub_feature_flags(achievements: false)
+ stub_feature_flags(achievements: group2)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return user_achievements for that namespace' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievement2)
+ )
+ end
+ end
+
+ context 'when current user is not a member of the private group' do
+ let(:current_user) { create(:user) }
+
+ specify { expect(graphql_data_at(:user, :userAchievements, :nodes)).to be_empty }
+ end
+end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 0fad4f4ff3a..24c72a8bb00 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -373,6 +373,32 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
end
+
+ describe 'notifications widget' do
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'NOTIFICATIONS',
+ 'subscribed' => work_item.subscribed?(current_user, project)
+ )
+ )
+ )
+ end
+ end
end
context 'when an Issue Global ID is provided' do
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index d7724371cce..8a3c5261eb6 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'GraphQL', feature_category: :not_owned do
+RSpec.describe 'GraphQL', feature_category: :shared do
include GraphqlHelpers
include AfterNextHelpers
diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb
index 91f64d02d43..2f05b0fcf21 100644
--- a/spec/requests/api/group_milestones_spec.rb
+++ b/spec/requests/api/group_milestones_spec.rb
@@ -4,35 +4,41 @@ require 'spec_helper'
RSpec.describe API::GroupMilestones, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group, :private) }
+ let_it_be_with_refind(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:group_member) { create(:group_member, group: group, user: user) }
- let_it_be(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') }
- let_it_be(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') }
+ let_it_be(:closed_milestone) do
+ create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone')
+ end
+
+ let_it_be_with_reload(:milestone) do
+ create(:milestone, group: group, title: 'version2', description: 'open milestone', updated_at: 4.days.ago)
+ end
let(:route) { "/groups/#{group.id}/milestones" }
+ shared_examples 'listing all milestones' do
+ it 'returns correct list of milestones' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(milestones.size)
+ expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
+ end
+ end
+
it_behaves_like 'group and project milestones', "/groups/:id/milestones"
describe 'GET /groups/:id/milestones' do
- context 'when include_parent_milestones is true' do
- let_it_be(:ancestor_group) { create(:group, :private) }
- let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) }
- let_it_be(:params) { { include_parent_milestones: true } }
-
- before_all do
- group.update!(parent: ancestor_group)
- end
+ let_it_be(:ancestor_group) { create(:group, :private) }
+ let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 2.days.ago) }
- shared_examples 'listing all milestones' do
- it 'returns correct list of milestones' do
- get api(route, user), params: params
+ before_all do
+ group.update!(parent: ancestor_group)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(milestones.size)
- expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
- end
- end
+ context 'when include_parent_milestones is true' do
+ let(:params) { { include_parent_milestones: true } }
context 'when user has access to ancestor groups' do
let(:milestones) { [ancestor_group_milestone, milestone, closed_milestone] }
@@ -45,10 +51,26 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do
it_behaves_like 'listing all milestones'
context 'when iids param is present' do
- let_it_be(:params) { { include_parent_milestones: true, iids: [milestone.iid] } }
+ let(:params) { { include_parent_milestones: true, iids: [milestone.iid] } }
it_behaves_like 'listing all milestones'
end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 1.day.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [ancestor_group_milestone, milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 1.day.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [closed_milestone] }
+ end
+ end
end
context 'when user has no access to ancestor groups' do
@@ -63,6 +85,22 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do
end
end
end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 1.day.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 1.day.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [closed_milestone] }
+ end
+ end
end
describe 'GET /groups/:id/milestones/:milestone_id/issues' do
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index e3d538d72ba..ff20e7ea9dd 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupVariables, feature_category: :pipeline_authoring do
+RSpec.describe API::GroupVariables, feature_category: :pipeline_composition do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:variable) { create(:ci_group_variable, group: group) }
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
index 584f6e3c7d4..d6afd6f86ff 100644
--- a/spec/requests/api/helm_packages_spec.rb
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -17,7 +17,15 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
let_it_be(:package_file2_2) { create(:helm_package_file, package: package2, file_sha256: 'file2', file_name: 'filename2.tgz', channel: 'test', description: 'hello from test channel') }
let_it_be(:other_package) { create(:npm_package, project: project) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_helm_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
+
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: project, namespace: project.namespace, property: 'i_package_helm_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_helm_user', user: user }
+ end
+ end
describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do
let(:project_id) { project.id }
@@ -65,6 +73,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
with_them do
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
before do
project.update!(visibility: visibility.to_s)
@@ -75,6 +84,8 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
end
context 'with access to package registry for everyone' do
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
+
before do
project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE)
project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
@@ -116,6 +127,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
with_them do
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
@@ -178,6 +190,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
with_them do
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 38275ce0057..0be9df41e8f 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'raven/transports/dummy'
require_relative '../../../config/initializers/sentry'
-RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :authentication_and_authorization do
+RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_access do
include API::APIGuard::HelperMethods
include described_class
include TermsHelper
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index ca32271f573..dff41c4c477 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Base, feature_category: :authentication_and_authorization do
+RSpec.describe API::Internal::Base, feature_category: :system_access do
include GitlabShellHelpers
include APIInternalBaseHelpers
@@ -514,7 +514,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(json_response["gl_key_type"]).to eq("key")
expect(json_response["gl_key_id"]).to eq(key.id)
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'sets hook env' do
@@ -553,7 +553,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(personal_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{personal_snippet.id}")
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'sets hook env' do
@@ -585,7 +585,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(project_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{project_snippet.id}")
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'sets hook env' do
@@ -703,7 +703,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'rate limited request' do
@@ -862,7 +862,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response['status']).to be_truthy
expect(json_response['payload']).to eql(payload)
expect(json_response['gl_console_messages']).to eql(console_messages)
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
end
end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index be76e55269a..547b9071f94 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -306,4 +306,151 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
end
end
end
+
+ describe 'POST /internal/kubernetes/authorize_proxy_user', :clean_gitlab_redis_sessions do
+ include SessionHelpers
+
+ def send_request(headers: {}, params: {})
+ post api('/internal/kubernetes/authorize_proxy_user'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
+ end
+
+ def stub_user_session(user, csrf_token)
+ stub_session(
+ {
+ 'warden.user.user.key' => [[user.id], user.authenticatable_salt],
+ '_csrf_token' => csrf_token
+ }
+ )
+ end
+
+ def stub_user_session_with_no_user_id(user, csrf_token)
+ stub_session(
+ {
+ 'warden.user.user.key' => [[nil], user.authenticatable_salt],
+ '_csrf_token' => csrf_token
+ }
+ )
+ end
+
+ def mask_token(encoded_token)
+ controller = ActionController::Base.new
+ raw_token = controller.send(:decode_csrf_token, encoded_token)
+ controller.send(:mask_token, raw_token)
+ end
+
+ def new_token
+ ActionController::Base.new.send(:generate_csrf_token)
+ end
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user_access_config) do
+ {
+ 'user_access' => {
+ 'access_as' => { 'agent' => {} },
+ 'projects' => [{ 'id' => project.full_path }],
+ 'groups' => [{ 'id' => group.full_path }]
+ }
+ }
+ end
+
+ let_it_be(:configuration_project) do
+ create(
+ :project, :custom_repo,
+ files: {
+ ".gitlab/agents/the-agent/config.yaml" => user_access_config.to_yaml
+ }
+ )
+ end
+
+ let_it_be(:agent) { create(:cluster_agent, name: 'the-agent', project: configuration_project) }
+ let_it_be(:another_agent) { create(:cluster_agent) }
+
+ let(:user) { create(:user) }
+
+ before do
+ allow(::Gitlab::Kas).to receive(:enabled?).and_return true
+ end
+
+ it 'returns 400 when cookie is invalid' do
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: '123', csrf_token: mask_token(new_token) })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 401 when session is not found' do
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id('abc')
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 401 when CSRF token does not match' do
+ public_id = stub_user_session(user, new_token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 404 for non-existent agent' do
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: non_existing_record_id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 403 when user has no access' do
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'returns 200 when user has access' do
+ project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ it 'returns 401 when user has valid KAS cookie and CSRF token but has no access to requested agent' do
+ project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: another_agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'returns 401 when global flag is disabled' do
+ stub_feature_flags(kas_user_access: false)
+
+ project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 401 when user id is not found in session' do
+ project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session_with_no_user_id(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 56f1089843b..20fb9100ebb 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -3,193 +3,97 @@
require 'spec_helper'
RSpec.describe API::Internal::Pages, feature_category: :pages do
- let(:auth_headers) do
- jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
- { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ let_it_be(:group) { create(:group, name: 'mygroup') }
+ let_it_be_with_reload(:project) { create(:project, name: 'myproject', group: group) }
+
+ let(:auth_header) do
+ {
+ Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => JWT.encode(
+ { 'iss' => 'gitlab-pages' },
+ Gitlab::Pages.secret, 'HS256')
+ }
end
- let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
-
before do
- allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret)
+ allow(Gitlab::Pages)
+ .to receive(:secret)
+ .and_return(SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH))
+
stub_pages_object_storage(::Pages::DeploymentUploader)
end
- describe "GET /internal/pages/status" do
- def query_enabled(headers = {})
- get api("/internal/pages/status"), headers: headers
- end
-
+ describe 'GET /internal/pages/status' do
it 'responds with 401 Unauthorized' do
- query_enabled
+ get api('/internal/pages/status')
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'responds with 204 no content' do
- query_enabled(auth_headers)
+ get api('/internal/pages/status'), headers: auth_header
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_empty
end
end
- describe "GET /internal/pages" do
- def query_host(host, headers = {})
- get api("/internal/pages"), headers: headers, params: { host: host }
- end
-
- around do |example|
- freeze_time do
- example.run
- end
- end
-
- context 'not authenticated' do
+ describe 'GET /internal/pages' do
+ context 'when not authenticated' do
it 'responds with 401 Unauthorized' do
- query_host('pages.gitlab.io')
+ get api('/internal/pages')
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
- context 'authenticated' do
- def query_host(host)
- jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
- headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
-
- super(host, headers)
+ context 'when authenticated' do
+ before do
+ project.update_pages_deployment!(create(:pages_deployment, project: project))
end
- def deploy_pages(project)
- deployment = create(:pages_deployment, project: project)
- project.mark_pages_as_deployed
- project.update_pages_deployment!(deployment)
+ around do |example|
+ freeze_time do
+ example.run
+ end
end
- context 'domain does not exist' do
+ context 'when domain does not exist' do
it 'responds with 204 no content' do
- query_host('pages.gitlab.io')
+ get api('/internal/pages'), headers: auth_header, params: { host: 'any-domain.gitlab.io' }
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_empty
end
end
- context 'serverless domain' do
- let(:namespace) { create(:namespace, name: 'gitlab-org') }
- let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
- let(:environment) { create(:environment, project: project) }
- let(:pages_domain) { create(:pages_domain, domain: 'serverless.gitlab.io') }
- let(:knative_without_ingress) { create(:clusters_applications_knative) }
- let(:knative_with_ingress) { create(:clusters_applications_knative, external_ip: '10.0.0.1') }
-
- context 'without a knative ingress gateway IP' do
- let!(:serverless_domain_cluster) do
- create(
- :serverless_domain_cluster,
- uuid: 'abcdef12345678',
- pages_domain: pages_domain,
- knative: knative_without_ingress
- )
- end
-
- let(:serverless_domain) do
- create(
- :serverless_domain,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- it 'responds with 204 no content' do
- query_host(serverless_domain.uri.host)
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(response.body).to be_empty
- end
- end
-
- context 'with a knative ingress gateway IP' do
- let!(:serverless_domain_cluster) do
- create(
- :serverless_domain_cluster,
- uuid: 'abcdef12345678',
- pages_domain: pages_domain,
- knative: knative_with_ingress
- )
- end
-
- let(:serverless_domain) do
- create(
- :serverless_domain,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- it 'responds with 204 because of feature deprecation' do
- query_host(serverless_domain.uri.host)
+ context 'when querying a custom domain' do
+ let_it_be(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect(response.body).to be_empty
-
- ##
- # Serverless serving and reverse proxy to Kubernetes / Knative has
- # been deprecated and disabled, as per
- # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467
- #
- # expect(response).to match_response_schema('internal/serverless/virtual_domain')
- # expect(json_response['certificate']).to eq(pages_domain.certificate)
- # expect(json_response['key']).to eq(pages_domain.key)
- #
- # expect(json_response['lookup_paths']).to eq(
- # [
- # {
- # 'source' => {
- # 'type' => 'serverless',
- # 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}",
- # 'cluster' => {
- # 'hostname' => serverless_domain_cluster.knative.hostname,
- # 'address' => serverless_domain_cluster.knative.external_ip,
- # 'port' => 443,
- # 'cert' => serverless_domain_cluster.certificate,
- # 'key' => serverless_domain_cluster.key
- # }
- # }
- # }
- # ]
- # )
+ context 'when there are no pages deployed for the related project' do
+ before do
+ project.mark_pages_as_not_deployed
end
- end
- end
- context 'custom domain' do
- let(:namespace) { create(:namespace, name: 'gitlab-org') }
- let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
- let!(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) }
-
- context 'when there are no pages deployed for the related project' do
it 'responds with 204 No Content' do
- query_host('pages.io')
+ get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' }
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when there are pages deployed for the related project' do
- it 'domain lookup is case insensitive' do
- deploy_pages(project)
+ before do
+ project.mark_pages_as_deployed
+ end
- query_host('Pages.IO')
+ it 'domain lookup is case insensitive' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'Pages.IO' }
expect(response).to have_gitlab_http_status(:ok)
end
it 'responds with the correct domain configuration' do
- deploy_pages(project)
-
- query_host('pages.io')
+ get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
@@ -212,7 +116,8 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_domain' => nil
}
]
)
@@ -220,20 +125,67 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
end
- context 'namespaced domain' do
- let(:group) { create(:group, name: 'mygroup') }
+ context 'when querying a unique domain' do
+ before_all do
+ project.project_setting.update!(
+ pages_unique_domain: 'unique-domain',
+ pages_unique_domain_enabled: true
+ )
+ end
- before do
- allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io')
- allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io")
+ context 'when there are no pages deployed for the related project' do
+ before do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
end
- context 'regular project' do
- it 'responds with the correct domain configuration' do
- project = create(:project, group: group, name: 'myproject')
- deploy_pages(project)
+ context 'when there are pages deployed for the related project' do
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
- query_host('mygroup.gitlab-pages.io')
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ context 'when the unique domain is disabled' do
+ before do
+ project.project_setting.update!(pages_unique_domain_enabled: false)
+ end
+
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ it 'domain lookup is case insensitive' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'Unique-Domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'responds with the correct domain configuration' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
@@ -245,7 +197,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'project_id' => project.id,
'access_control' => false,
'https_only' => false,
- 'prefix' => '/myproject/',
+ 'prefix' => '/',
'source' => {
'type' => 'zip',
'path' => deployment.file.url(expire_at: 1.day.from_now),
@@ -253,56 +205,116 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_domain' => 'unique-domain'
}
]
)
end
end
+ end
- it 'avoids N+1 queries' do
- project = create(:project, group: group)
- deploy_pages(project)
-
- control = ActiveRecord::QueryRecorder.new { query_host('mygroup.gitlab-pages.io') }
+ context 'when querying a namespaced domain' do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io')
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io")
+ end
- 3.times do
- project = create(:project, group: group)
- deploy_pages(project)
+ context 'when there are no pages deployed for the related project' do
+ before do
+ project.mark_pages_as_not_deployed
end
- expect { query_host('mygroup.gitlab-pages.io') }.not_to exceed_query_limit(control)
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'mygroup.gitlab-pages.io' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+ expect(json_response['lookup_paths']).to eq([])
+ end
end
- context 'group root project' do
- it 'responds with the correct domain configuration' do
- project = create(:project, group: group, name: 'mygroup.gitlab-pages.io')
- deploy_pages(project)
+ context 'when there are pages deployed for the related project' do
+ before do
+ project.mark_pages_as_deployed
+ end
- query_host('mygroup.gitlab-pages.io')
+ context 'with a regular project' do
+ it 'responds with the correct domain configuration' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'mygroup.gitlab-pages.io' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ deployment = project.pages_metadatum.pages_deployment
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/myproject/',
+ 'source' => {
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
+ },
+ 'unique_domain' => nil
+ }
+ ]
+ )
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('internal/pages/virtual_domain')
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'mygroup.gitlab-pages.io' }
+ end
- deployment = project.pages_metadatum.pages_deployment
- expect(json_response['lookup_paths']).to eq(
- [
- {
- 'project_id' => project.id,
- 'access_control' => false,
- 'https_only' => false,
- 'prefix' => '/',
- 'source' => {
- 'type' => 'zip',
- 'path' => deployment.file.url(expire_at: 1.day.from_now),
- 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
- 'sha256' => deployment.file_sha256,
- 'file_size' => deployment.size,
- 'file_count' => deployment.file_count
+ 3.times do
+ project = create(:project, group: group)
+ project.mark_pages_as_deployed
+ end
+
+ expect { get api('/internal/pages'), headers: auth_header, params: { host: 'mygroup.gitlab-pages.io' } }
+ .not_to exceed_query_limit(control)
+ end
+
+ context 'with a group root project' do
+ before do
+ project.update!(path: 'mygroup.gitlab-pages.io')
+ end
+
+ it 'responds with the correct domain configuration' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'mygroup.gitlab-pages.io' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ deployment = project.pages_metadatum.pages_deployment
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/',
+ 'source' => {
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
+ },
+ 'unique_domain' => nil
}
- }
- ]
- )
+ ]
+ )
+ end
end
end
end
diff --git a/spec/requests/api/internal/workhorse_spec.rb b/spec/requests/api/internal/workhorse_spec.rb
index 99d0ecabbb7..2657abffae6 100644
--- a/spec/requests/api/internal/workhorse_spec.rb
+++ b/spec/requests/api/internal/workhorse_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :not_owned do
+RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :shared do
include WorkhorseHelpers
context '/authorize_upload' do
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 0641c2135c1..eaa3c46d0ca 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:base_url) { "/groups/#{group.id}/issues" }
shared_examples 'group issues statistics' do
- it 'returns issues statistics' do
+ it 'returns issues statistics', :aggregate_failures do
get api("/groups/#{group.id}/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -346,7 +346,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
group_project.add_reporter(user)
end
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api(base_url, admin)
expect(response).to have_gitlab_http_status(:ok)
@@ -355,7 +355,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns all group issues (including opened and closed)' do
- get api(base_url, admin)
+ get api(base_url, admin, admin_mode: true)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
@@ -385,7 +385,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns group confidential issues for admin' do
- get api(base_url, admin), params: { state: :opened }
+ get api(base_url, admin, admin_mode: true), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
@@ -403,7 +403,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'labels parameter' do
- it 'returns an array of labeled group issues' do
+ it 'returns an array of labeled group issues', :aggregate_failures do
get api(base_url, user), params: { labels: group_label.title }
expect_paginated_array_response(group_issue.id)
@@ -486,7 +486,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'returns an array of issues found by iids' do
+ it 'returns an array of issues found by iids', :aggregate_failures do
get api(base_url, user), params: { iids: [group_issue.iid] }
expect_paginated_array_response(group_issue.id)
@@ -505,14 +505,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response([])
end
- it 'returns an array of group issues with any label' do
+ it 'returns an array of group issues with any label', :aggregate_failures do
get api(base_url, user), params: { labels: IssuableFinder::Params::FILTER_ANY }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
- it 'returns an array of group issues with any label with labels param as array' do
+ it 'returns an array of group issues with any label with labels param as array', :aggregate_failures do
get api(base_url, user), params: { labels: [IssuableFinder::Params::FILTER_ANY] }
expect_paginated_array_response(group_issue.id)
@@ -555,7 +555,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response(group_closed_issue.id)
end
- it 'returns an array of issues with no milestone' do
+ it 'returns an array of issues with no milestone', :aggregate_failures do
get api(base_url, user), params: { milestone: no_milestone_title }
expect(response).to have_gitlab_http_status(:ok)
@@ -688,28 +688,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) }
- it 'returns issues with by assignee_username' do
+ it 'returns issues with by assignee_username', :aggregate_failures do
get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([issue3.id, group_confidential_issue.id])
end
- it 'returns issues by assignee_username as string' do
+ it 'returns issues by assignee_username as string', :aggregate_failures do
get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([issue3.id, group_confidential_issue.id])
end
- it 'returns error when multiple assignees are passed' do
+ it 'returns error when multiple assignees are passed', :aggregate_failures do
get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("allows one value, but found 2")
end
- it 'returns error when assignee_username and assignee_id are passed together' do
+ it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do
get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -719,7 +719,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe "#to_reference" do
- it 'exposes reference path in context of group' do
+ it 'exposes reference path in context of group', :aggregate_failures do
get api(base_url, user)
expect(json_response.first['references']['short']).to eq("##{group_closed_issue.iid}")
@@ -735,7 +735,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
group_closed_issue.reload
end
- it 'exposes reference path in context of parent group' do
+ it 'exposes reference path in context of parent group', :aggregate_failures do
get api("/groups/#{parent_group.id}/issues")
expect(json_response.first['references']['short']).to eq("##{group_closed_issue.iid}")
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index 6fc3903103b..915b8fff75e 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
shared_examples 'project issues statistics' do
- it 'returns project issues statistics' do
+ it 'returns project issues statistics', :aggregate_failures do
get api("/projects/#{project.id}/issues_statistics", current_user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -317,7 +317,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns project confidential issues for admin' do
- get api("#{base_url}/issues", admin)
+ get api("#{base_url}/issues", admin, admin_mode: true)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
@@ -526,7 +526,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
end
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api("#{base_url}/issues", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -607,28 +607,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
- it 'returns issues by assignee_username' do
+ it 'returns issues by assignee_username', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns issues by assignee_username as string' do
+ it 'returns issues by assignee_username as string', :aggregate_failures do
get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns error when multiple assignees are passed' do
+ it 'returns error when multiple assignees are passed', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("allows one value, but found 2")
end
- it 'returns error when assignee_username and assignee_id are passed together' do
+ it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -646,7 +646,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -686,7 +686,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'exposes the closed_at attribute' do
+ it 'exposes the closed_at attribute', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -694,7 +694,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'links exposure' do
- it 'exposes related resources full URIs' do
+ it 'exposes related resources full URIs', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
links = json_response['_links']
@@ -706,7 +706,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'returns a project issue by internal id' do
+ it 'returns a project issue by internal id', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -738,7 +738,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'returns confidential issue for project members' do
+ it 'returns confidential issue for project members', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -746,7 +746,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it 'returns confidential issue for author' do
+ it 'returns confidential issue for author', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
expect(response).to have_gitlab_http_status(:ok)
@@ -754,7 +754,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it 'returns confidential issue for assignee' do
+ it 'returns confidential issue for assignee', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
expect(response).to have_gitlab_http_status(:ok)
@@ -762,8 +762,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it 'returns confidential issue for admin' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
+ it 'returns confidential issue for admin', :aggregate_failures do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(confidential_issue.title)
@@ -829,7 +829,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:related_mr) { create_referencing_mr(user, project, issue) }
context 'when unauthenticated' do
- it 'return list of referenced merge requests from issue' do
+ it 'return list of referenced merge requests from issue', :aggregate_failures do
get_related_merge_requests(project.id, issue.iid)
expect_paginated_array_response(related_mr.id)
@@ -898,8 +898,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
+ it 'exposes known attributes', :aggregate_failures do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
@@ -936,7 +936,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
)
end
- it 'returns a full list of participants' do
+ it 'returns a full list of participants', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}/participants", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -945,7 +945,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when user cannot see a confidential note' do
- it 'returns a limited list of participants' do
+ it 'returns a limited list of participants', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}/participants", create(:user))
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 4b60eaadcbc..33f49cefc69 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
shared_examples 'issues statistics' do
- it 'returns issues statistics' do
+ it 'returns issues statistics', :aggregate_failures do
get api("/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -109,8 +109,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'as an admin' do
context 'when issue exists' do
- it 'returns the issue' do
- get api("/issues/#{issue.id}", admin)
+ it 'returns the issue', :aggregate_failures do
+ get api("/issues/#{issue.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.dig('author', 'id')).to eq(issue.author.id)
@@ -121,7 +121,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when issue does not exist' do
it 'returns 404' do
- get api("/issues/0", admin)
+ get api("/issues/0", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -132,7 +132,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'GET /issues' do
context 'when unauthenticated' do
- it 'returns an array of all issues' do
+ it 'returns an array of all issues', :aggregate_failures do
get api('/issues'), params: { scope: 'all' }
expect(response).to have_gitlab_http_status(:ok)
@@ -162,14 +162,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:unauthorized)
end
- it 'returns an array of issues matching state in milestone' do
+ it 'returns an array of issues matching state in milestone', :aggregate_failures do
get api('/issues'), params: { milestone: 'foo', scope: 'all' }
expect(response).to have_gitlab_http_status(:ok)
expect_paginated_array_response([])
end
- it 'returns an array of issues matching state in milestone' do
+ it 'returns an array of issues matching state in milestone', :aggregate_failures do
get api('/issues'), params: { milestone: milestone.title, scope: 'all' }
expect(response).to have_gitlab_http_status(:ok)
@@ -273,7 +273,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when authenticated' do
- it 'returns an array of issues' do
+ it 'returns an array of issues', :aggregate_failures do
get api('/issues', user)
expect_paginated_array_response([issue.id, closed_issue.id])
@@ -532,7 +532,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'with incident issues' do
let_it_be(:incident) { create(:incident, project: project) }
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
get api('/issues', user) # warm up
control = ActiveRecord::QueryRecorder.new do
@@ -553,7 +553,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'with issues closed as duplicates' do
let_it_be(:dup_issue_1) { create(:issue, :closed_as_duplicate, project: project) }
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
get api('/issues', user) # warm up
control = ActiveRecord::QueryRecorder.new do
@@ -639,7 +639,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response([])
end
- it 'returns an array of labeled issues matching given state' do
+ it 'returns an array of labeled issues matching given state', :aggregate_failures do
get api('/issues', user), params: { labels: label.title, state: :opened }
expect_paginated_array_response(issue.id)
@@ -647,7 +647,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response.first['state']).to eq('opened')
end
- it 'returns an array of labeled issues matching given state with labels param as array' do
+ it 'returns an array of labeled issues matching given state with labels param as array', :aggregate_failures do
get api('/issues', user), params: { labels: [label.title], state: :opened }
expect_paginated_array_response(issue.id)
@@ -917,14 +917,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'matches V4 response schema' do
+ it 'matches V4 response schema', :aggregate_failures do
get api('/issues', user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/issues')
end
- it 'returns a related merge request count of 0 if there are no related merge requests' do
+ it 'returns a related merge request count of 0 if there are no related merge requests', :aggregate_failures do
get api('/issues', user)
expect(response).to have_gitlab_http_status(:ok)
@@ -932,7 +932,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response.first).to include('merge_requests_count' => 0)
end
- it 'returns a related merge request count > 0 if there are related merge requests' do
+ it 'returns a related merge request count > 0 if there are related merge requests', :aggregate_failures do
create(:merge_requests_closing_issues, issue: issue)
get api('/issues', user)
@@ -1013,28 +1013,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
- it 'returns issues with by assignee_username' do
+ it 'returns issues with by assignee_username', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns issues by assignee_username as string' do
+ it 'returns issues by assignee_username as string', :aggregate_failures do
get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns error when multiple assignees are passed' do
+ it 'returns error when multiple assignees are passed', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("allows one value, but found 2")
end
- it 'returns error when assignee_username and assignee_id are passed together' do
+ it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1088,7 +1088,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'GET /projects/:id/issues/:issue_iid' do
- it 'exposes full reference path' do
+ it 'exposes full reference path', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1106,7 +1106,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'user does not have permission to view new issue' do
- it 'does not return the issue as closed_as_duplicate_of' do
+ it 'does not return the issue as closed_as_duplicate_of', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1119,7 +1119,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
new_issue.project.add_guest(user)
end
- it 'returns the issue as closed_as_duplicate_of' do
+ it 'returns the issue as closed_as_duplicate_of', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1131,7 +1131,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe "POST /projects/:id/issues" do
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(:created)
@@ -1139,6 +1139,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['issue_type']).to eq('issue')
end
+ context 'when confidential is null' do
+ it 'responds with 400 error', :aggregate_failures do
+ post api("/projects/#{project.id}/issues", user), params: { title: 'issue', confidential: nil }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('confidential is empty')
+ end
+ end
+
context 'when issue create service returns an unrecoverable error' do
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
@@ -1146,7 +1155,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'returns and error message and status code from the service' do
+ it 'returns and error message and status code from the service', :aggregate_failures do
post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1168,15 +1177,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do
travel_to fixed_time
end
- it 'allows admins to set the timestamp' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", admin), params: { labels: 'label1', updated_at: updated_at }
+ it 'allows admins to set the timestamp', :aggregate_failures do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", admin, admin_mode: true), params: { labels: 'label1', updated_at: updated_at }
expect(response).to have_gitlab_http_status(:ok)
expect(Time.parse(json_response['updated_at'])).to be_like_time(updated_at)
expect(ResourceLabelEvent.last.created_at).to be_like_time(updated_at)
end
- it 'does not allow other users to set the timestamp' do
+ it 'does not allow other users to set the timestamp', :aggregate_failures do
reporter = create(:user)
project.add_developer(reporter)
@@ -1259,7 +1268,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'with valid params' do
- it 'reorders issues and returns a successful 200 response' do
+ it 'reorders issues and returns a successful 200 response', :aggregate_failures do
put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -1286,7 +1295,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:other_project) { create(:project, group: group) }
let(:other_issue) { create(:issue, project: other_project, relative_position: 80) }
- it 'reorders issues and returns a successful 200 response' do
+ it 'reorders issues and returns a successful 200 response', :aggregate_failures do
put api("/projects/#{other_project.id}/issues/#{other_issue.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 265091fa698..a17c1389e83 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'POST /projects/:id/issues' do
context 'support for deprecated assignee_id' do
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_id: user2.id }
@@ -85,7 +85,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
- it 'creates a new project issue when assignee_id is empty' do
+ it 'creates a new project issue when assignee_id is empty', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_id: '' }
@@ -96,7 +96,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'single assignee restrictions' do
- it 'creates a new project issue with no more than one assignee' do
+ it 'creates a new project issue with no more than one assignee', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
@@ -122,8 +122,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'an internal ID is provided' do
context 'by an admin' do
- it 'sets the internal ID on the new issue' do
- post api("/projects/#{project.id}/issues", admin),
+ it 'sets the internal ID on the new issue', :aggregate_failures do
+ post api("/projects/#{project.id}/issues", admin, admin_mode: true),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(:created)
@@ -132,7 +132,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'by an owner' do
- it 'sets the internal ID on the new issue' do
+ it 'sets the internal ID on the new issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', iid: 9001 }
@@ -145,7 +145,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:group) { create(:group) }
let(:group_project) { create(:project, :public, namespace: group) }
- it 'sets the internal ID on the new issue' do
+ it 'sets the internal ID on the new issue', :aggregate_failures do
group.add_owner(user2)
post api("/projects/#{group_project.id}/issues", user2),
params: { title: 'new issue', iid: 9001 }
@@ -156,7 +156,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'by another user' do
- it 'ignores the given internal ID' do
+ it 'ignores the given internal ID', :aggregate_failures do
post api("/projects/#{project.id}/issues", user2),
params: { title: 'new issue', iid: 9001 }
@@ -166,8 +166,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when an issue with the same IID exists on database' do
- it 'returns 409' do
- post api("/projects/#{project.id}/issues", admin),
+ it 'returns 409', :aggregate_failures do
+ post api("/projects/#{project.id}/issues", admin, admin_mode: true),
params: { title: 'new issue', iid: issue.iid }
expect(response).to have_gitlab_http_status(:conflict)
@@ -176,7 +176,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
@@ -189,7 +189,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
- it 'creates a new project issue with labels param as array' do
+ it 'creates a new project issue with labels param as array', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
@@ -202,7 +202,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
- it 'creates a new confidential project issue' do
+ it 'creates a new confidential project issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: true }
@@ -211,7 +211,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['confidential']).to be_truthy
end
- it 'creates a new confidential project issue with a different param' do
+ it 'creates a new confidential project issue with a different param', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: 'y' }
@@ -220,7 +220,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['confidential']).to be_truthy
end
- it 'creates a public issue when confidential param is false' do
+ it 'creates a public issue when confidential param is false', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: false }
@@ -229,7 +229,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['confidential']).to be_falsy
end
- it 'creates a public issue when confidential param is invalid' do
+ it 'creates a public issue when confidential param is invalid', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'new issue', confidential: 'foo' }
@@ -242,7 +242,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'allows special label names' do
+ it 'allows special label names', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
@@ -256,7 +256,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['labels']).to include '&'
end
- it 'allows special label names with labels param as array' do
+ it 'allows special label names with labels param as array', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: {
title: 'new issue',
@@ -270,7 +270,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['labels']).to include '&'
end
- it 'returns 400 if title is too long' do
+ it 'returns 400 if title is too long', :aggregate_failures do
post api("/projects/#{project.id}/issues", user),
params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -313,7 +313,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'with due date' do
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
post api("/projects/#{project.id}/issues", user),
@@ -336,8 +336,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'by an admin' do
- it 'sets the creation time on the new issue' do
- post api("/projects/#{project.id}/issues", admin), params: params
+ it 'sets the creation time on the new issue', :aggregate_failures do
+ post api("/projects/#{project.id}/issues", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
@@ -346,7 +346,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'by a project owner' do
- it 'sets the creation time on the new issue' do
+ it 'sets the creation time on the new issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user), params: params
expect(response).to have_gitlab_http_status(:created)
@@ -356,7 +356,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'by a group owner' do
- it 'sets the creation time on the new issue' do
+ it 'sets the creation time on the new issue', :aggregate_failures do
group = create(:group)
group_project = create(:project, :public, namespace: group)
group.add_owner(user2)
@@ -370,7 +370,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'by another user' do
- it 'ignores the given creation time' do
+ it 'ignores the given creation time', :aggregate_failures do
project.add_developer(user2)
post api("/projects/#{project.id}/issues", user2), params: params
@@ -397,7 +397,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when request exceeds the rate limit' do
- it 'prevents users from creating more issues' do
+ it 'prevents users from creating more issues', :aggregate_failures do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
post api("/projects/#{project.id}/issues", user),
@@ -437,7 +437,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect { post_issue }.not_to change(Issue, :count)
end
- it 'returns correct status and message' do
+ it 'returns correct status and message', :aggregate_failures do
post_issue
expect(response).to have_gitlab_http_status(:bad_request)
@@ -476,7 +476,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace) }
- it 'moves an issue' do
+ it 'moves an issue', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project.id }
@@ -485,7 +485,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when source and target projects are the same' do
- it 'returns 400 when trying to move an issue' do
+ it 'returns 400 when trying to move an issue', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: project.id }
@@ -495,7 +495,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when the user does not have the permission to move issues' do
- it 'returns 400 when trying to move an issue' do
+ it 'returns 400 when trying to move an issue', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project2.id }
@@ -504,8 +504,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'moves the issue to another namespace if I am admin' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
+ it 'moves the issue to another namespace if I am admin', :aggregate_failures do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin, admin_mode: true),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(:created)
@@ -513,7 +513,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when using the issue ID instead of iid' do
- it 'returns 404 when trying to move an issue', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do
+ it 'returns 404 when trying to move an issue', :aggregate_failures, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
params: { to_project_id: target_project.id }
@@ -523,7 +523,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
+ it 'returns 404 when trying to move an issue', :aggregate_failures do
post api("/projects/#{project.id}/issues/123/move", user),
params: { to_project_id: target_project.id }
@@ -533,7 +533,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when source project does not exist' do
- it 'returns 404 when trying to move an issue' do
+ it 'returns 404 when trying to move an issue', :aggregate_failures do
post api("/projects/0/issues/#{issue.iid}/move", user),
params: { to_project_id: target_project.id }
@@ -562,7 +562,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when user can admin the issue' do
context 'when the user can admin the target project' do
- it 'clones the issue' do
+ it 'clones the issue', :aggregate_failures do
expect do
post_clone_issue(user, issue, valid_target_project)
end.to change { valid_target_project.issues.count }.by(1)
@@ -577,7 +577,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when target project is the same source project' do
- it 'clones the issue' do
+ it 'clones the issue', :aggregate_failures do
expect do
post_clone_issue(user, issue, issue.project)
end.to change { issue.reset.project.issues.count }.by(1)
@@ -595,7 +595,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when the user does not have the permission to clone issues' do
- it 'returns 400' do
+ it 'returns 400', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user),
params: { to_project_id: invalid_target_project.id }
@@ -605,7 +605,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when using the issue ID instead of iid' do
- it 'returns 404', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do
+ it 'returns 404', :aggregate_failures, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do
post api("/projects/#{project.id}/issues/#{issue.id}/clone", user),
params: { to_project_id: valid_target_project.id }
@@ -615,7 +615,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when issue does not exist' do
- it 'returns 404' do
+ it 'returns 404', :aggregate_failures do
post api("/projects/#{project.id}/issues/12300/clone", user),
params: { to_project_id: valid_target_project.id }
@@ -625,7 +625,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when source project does not exist' do
- it 'returns 404' do
+ it 'returns 404', :aggregate_failures do
post api("/projects/0/issues/#{issue.iid}/clone", user),
params: { to_project_id: valid_target_project.id }
@@ -635,7 +635,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when target project does not exist' do
- it 'returns 404' do
+ it 'returns 404', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user),
params: { to_project_id: 0 }
@@ -644,7 +644,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'clones the issue with notes when with_notes is true' do
+ it 'clones the issue with notes when with_notes is true', :aggregate_failures do
expect do
post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user),
params: { to_project_id: valid_target_project.id, with_notes: true }
@@ -661,7 +661,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'POST :id/issues/:issue_iid/subscribe' do
- it 'subscribes to an issue' do
+ it 'subscribes to an issue', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
expect(response).to have_gitlab_http_status(:created)
@@ -694,7 +694,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'POST :id/issues/:issue_id/unsubscribe' do
- it 'unsubscribes from an issue' do
+ it 'unsubscribes from an issue', :aggregate_failures do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
expect(response).to have_gitlab_http_status(:created)
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index f0d174c9e78..6cc639c0bcc 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'PUT /projects/:id/issues/:issue_iid to update only title' do
- it 'updates a project issue' do
+ it 'updates a project issue', :aggregate_failures do
put api_for_user, params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
@@ -109,7 +109,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'allows special label names with labels param as array' do
+ it 'allows special label names with labels param as array', :aggregate_failures do
put api_for_user,
params: {
title: updated_title,
@@ -135,42 +135,42 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'updates a confidential issue for project members' do
+ it 'updates a confidential issue for project members', :aggregate_failures do
put api(confidential_issue_path, user), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(updated_title)
end
- it 'updates a confidential issue for author' do
+ it 'updates a confidential issue for author', :aggregate_failures do
put api(confidential_issue_path, author), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(updated_title)
end
- it 'updates a confidential issue for admin' do
- put api(confidential_issue_path, admin), params: { title: updated_title }
+ it 'updates a confidential issue for admin', :aggregate_failures do
+ put api(confidential_issue_path, admin, admin_mode: true), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(updated_title)
end
- it 'sets an issue to confidential' do
+ it 'sets an issue to confidential', :aggregate_failures do
put api_for_user, params: { confidential: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be_truthy
end
- it 'makes a confidential issue public' do
+ it 'makes a confidential issue public', :aggregate_failures do
put api(confidential_issue_path, user), params: { confidential: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be_falsy
end
- it 'does not update a confidential issue with wrong confidential flag' do
+ it 'does not update a confidential issue with wrong confidential flag', :aggregate_failures do
put api(confidential_issue_path, user), params: { confidential: 'foo' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -209,7 +209,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect { update_issue }.not_to change { issue.reload.title }
end
- it 'returns correct status and message' do
+ it 'returns correct status and message', :aggregate_failures do
update_issue
expect(response).to have_gitlab_http_status(:bad_request)
@@ -246,14 +246,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
context 'support for deprecated assignee_id' do
- it 'removes assignee' do
+ it 'removes assignee', :aggregate_failures do
put api_for_user, params: { assignee_id: 0 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignee']).to be_nil
end
- it 'updates an issue with new assignee' do
+ it 'updates an issue with new assignee', :aggregate_failures do
put api_for_user, params: { assignee_id: user2.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -261,21 +261,21 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'removes assignee' do
+ it 'removes assignee', :aggregate_failures do
put api_for_user, params: { assignee_ids: [0] }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignees']).to be_empty
end
- it 'updates an issue with new assignee' do
+ it 'updates an issue with new assignee', :aggregate_failures do
put api_for_user, params: { assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
- context 'single assignee restrictions' do
+ context 'single assignee restrictions', :aggregate_failures do
it 'updates an issue with several assignees but only one has been applied' do
put api_for_user, params: { assignee_ids: [user2.id, guest.id] }
@@ -289,7 +289,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
- it 'adds relevant labels' do
+ it 'adds relevant labels', :aggregate_failures do
put api_for_user, params: { add_labels: '1, 2' }
expect(response).to have_gitlab_http_status(:ok)
@@ -300,14 +300,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:label2) { create(:label, title: 'a-label', project: project) }
let!(:label_link2) { create(:label_link, label: label2, target: issue) }
- it 'removes relevant labels' do
+ it 'removes relevant labels', :aggregate_failures do
put api_for_user, params: { remove_labels: label2.title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
- it 'removes all labels' do
+ it 'removes all labels', :aggregate_failures do
put api_for_user, params: { remove_labels: "#{label.title}, #{label2.title}" }
expect(response).to have_gitlab_http_status(:ok)
@@ -315,14 +315,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'does not update labels if not present' do
+ it 'does not update labels if not present', :aggregate_failures do
put api_for_user, params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
- it 'removes all labels and touches the record' do
+ it 'removes all labels and touches the record', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: '' }
end
@@ -332,7 +332,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'removes all labels and touches the record with labels param as array' do
+ it 'removes all labels and touches the record with labels param as array', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: [''] }
end
@@ -342,7 +342,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'updates labels and touches the record' do
+ it 'updates labels and touches the record', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: 'foo,bar' }
end
@@ -352,7 +352,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'updates labels and touches the record with labels param as array' do
+ it 'updates labels and touches the record with labels param as array', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: %w(foo bar) }
end
@@ -363,21 +363,21 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'allows special label names' do
+ it 'allows special label names', :aggregate_failures do
put api_for_user, params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&')
end
- it 'allows special label names with labels param as array' do
+ it 'allows special label names with labels param as array', :aggregate_failures do
put api_for_user, params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&')
end
- it 'returns 400 if title is too long' do
+ it 'returns 400 if title is too long', :aggregate_failures do
put api_for_user, params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -386,7 +386,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do
- it 'updates a project issue' do
+ it 'updates a project issue', :aggregate_failures do
put api_for_user, params: { labels: 'label2', state_event: 'close' }
expect(response).to have_gitlab_http_status(:ok)
@@ -394,7 +394,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['state']).to eq 'closed'
end
- it 'reopens a project isssue' do
+ it 'reopens a project isssue', :aggregate_failures do
put api(issue_path, user), params: { state_event: 'reopen' }
expect(response).to have_gitlab_http_status(:ok)
@@ -404,7 +404,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do
context 'when reporter makes request' do
- it 'accepts the update date to be set' do
+ it 'accepts the update date to be set', :aggregate_failures do
update_time = 2.weeks.ago
put api_for_user, params: { title: 'some new title', updated_at: update_time }
@@ -436,7 +436,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'accepts the update date to be set' do
+ it 'accepts the update date to be set', :aggregate_failures do
update_time = 2.weeks.ago
put api_for_owner, params: { title: 'some new title', updated_at: update_time }
@@ -448,7 +448,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
put api_for_user, params: { due_date: due_date }
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index d9a0f061156..0ca1a7d030f 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
+RSpec.describe API::Keys, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:email) { create(:email, user: user) }
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 82b87007a9b..3f131862a41 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Lint, feature_category: :pipeline_authoring do
+RSpec.describe API::Lint, feature_category: :pipeline_composition do
describe 'POST /ci/lint' do
context 'when signup settings are disabled' do
before do
@@ -245,8 +245,8 @@ RSpec.describe API::Lint, feature_category: :pipeline_authoring do
it 'passes validation' do
ci_lint
- included_config = YAML.safe_load(included_content, [Symbol])
- root_config = YAML.safe_load(yaml_content, [Symbol])
+ included_config = YAML.safe_load(included_content, permitted_classes: [Symbol])
+ root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml
expect(response).to have_gitlab_http_status(:ok)
@@ -535,8 +535,8 @@ RSpec.describe API::Lint, feature_category: :pipeline_authoring do
it 'passes validation' do
ci_lint
- included_config = YAML.safe_load(included_content, [Symbol])
- root_config = YAML.safe_load(yaml_content, [Symbol])
+ included_config = YAML.safe_load(included_content, permitted_classes: [Symbol])
+ root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 20aa660d95b..7b850fed79c 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } }
let(:package_name) { 'com/example/my-app' }
let(:headers) { workhorse_headers }
@@ -285,6 +285,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file' do
@@ -451,6 +453,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'forwarding package requests'
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file for a group' do
@@ -660,6 +664,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
subject { download_file(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@@ -901,8 +907,6 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'package workhorse uploads'
context 'event tracking' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } }
-
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
context 'when the package file fails to be created' do
@@ -962,6 +966,17 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ context 'file name is too long' do
+ let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) }
+
+ it 'rejects request' do
+ expect { upload_file_with_token(params: params, file_name: file_name) }.not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('File name is too long')
+ end
+ end
+
context 'version is not correct' do
let(:version) { '$%123' }
@@ -981,9 +996,9 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
package_settings.update!(maven_duplicates_allowed: false)
end
- shared_examples 'storing the package file' do
+ shared_examples 'storing the package file' do |file_name: 'my-app-1.0-20180724.124855-1'|
it 'stores the file', :aggregate_failures do
- expect { upload_file_with_token(params: params) }.to change { package.package_files.count }.by(1)
+ expect { upload_file_with_token(params: params, file_name: file_name) }.to change { package.package_files.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
expect(jar_file.file_name).to eq(file_upload.original_filename)
@@ -1023,6 +1038,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'storing the package file'
end
+
+ context 'when uploading a similar package file name with a classifier' do
+ it_behaves_like 'storing the package file', file_name: 'my-app-1.0-20180724.124855-1-javadoc'
+ end
end
context 'for sha1 file' do
@@ -1088,8 +1107,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
end
- def upload_file(params: {}, request_headers: headers, file_extension: 'jar')
- url = "/projects/#{project.id}/packages/maven/#{param_path}/my-app-1.0-20180724.124855-1.#{file_extension}"
+ def upload_file(params: {}, request_headers: headers, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1')
+ url = "/projects/#{project.id}/packages/maven/#{param_path}/#{file_name}.#{file_extension}"
workhorse_finalize(
api(url),
method: :put,
@@ -1100,8 +1119,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
)
end
- def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar')
- upload_file(params: params, request_headers: request_headers, file_extension: file_extension)
+ def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1')
+ upload_file(params: params, request_headers: request_headers, file_name: file_name, file_extension: file_extension)
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 19a630e5218..81815fdab62 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -168,6 +168,17 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
end
+ context 'when DB timeouts occur' do
+ it 'returns a :request_timeout status' do
+ allow(MergeRequestsFinder).to receive(:new).and_raise(ActiveRecord::QueryCanceled)
+
+ path = endpoint_path + '?view=simple'
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(:request_timeout)
+ end
+ end
+
it 'returns an array of all merge_requests using simple mode' do
path = endpoint_path + '?view=simple'
diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb
index b9bdadb01cc..e15186c48a5 100644
--- a/spec/requests/api/metadata_spec.rb
+++ b/spec/requests/api/metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Metadata, feature_category: :not_owned do
+RSpec.describe API::Metadata, feature_category: :shared do
shared_examples_for 'GET /metadata' do
context 'when unauthenticated' do
it 'returns authentication error' do
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index dcd2e4ae677..591a8ee68dc 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -11,8 +11,38 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests', scope: :instance do
- let(:url) { api("/packages/npm/#{package_name}") }
+ let(:url) { api("/packages/npm/#{package_name}") }
+
+ it_behaves_like 'handling get metadata requests', scope: :instance
+
+ context 'with a duplicate package name in another project' do
+ subject { get(url) }
+
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) do
+ create(:npm_package,
+ project: project2,
+ name: "@#{group.path}/scoped_package",
+ version: '1.2.0')
+ end
+
+ it 'includes all matching package versions in the response' do
+ subject
+
+ expect(json_response['versions'].keys).to match_array([package.version, package2.version])
+ end
+
+ context 'with the feature flag disabled' do
+ before do
+ stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
+ end
+
+ it 'returns matching package versions from only one project' do
+ subject
+
+ expect(json_response['versions'].keys).to match_array([package2.version])
+ end
+ end
end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index c62c0849776..2f67e1e8eea 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -115,6 +115,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
context 'private project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } }
+
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
@@ -143,6 +145,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
context 'internal project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } }
+
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index 4335ad75ab6..facbc01220d 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -12,8 +12,17 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
let_it_be(:deploy_token) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) }
- let(:snowplow_gitlab_standard_context) { { namespace: project.group, property: 'i_package_nuget_user' } }
let(:target_type) { 'groups' }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
+ let(:target) { subgroup }
+
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { namespace: target, property: 'i_package_nuget_user' }
+ else
+ { namespace: target, property: 'i_package_nuget_user', user: user }
+ end
+ end
shared_examples 'handling all endpoints' do
describe 'GET /api/v4/groups/:id/-/packages/nuget' do
@@ -84,7 +93,6 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
context 'a group' do
let(:target) { group }
- let(:snowplow_gitlab_standard_context) { { namespace: target, property: 'i_package_nuget_user' } }
it_behaves_like 'handling all endpoints'
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index 1e0d35ad451..887dfd4beeb 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -13,7 +13,15 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
let(:target) { project }
let(:target_type) { 'projects' }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_nuget_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
+
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: target, namespace: target.namespace, property: 'i_package_nuget_user' }
+ else
+ { project: target, namespace: target.namespace, property: 'i_package_nuget_user', user: user }
+ end
+ end
shared_examples 'accept get request on private project with access to package registry for everyone' do
subject { get api(url) }
@@ -149,6 +157,7 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index b29f1e9e661..19a943477d2 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorization do
+RSpec.describe 'OAuth tokens', feature_category: :system_access do
include HttpBasicAuthHelpers
context 'Resource Owner Password Credentials' do
@@ -124,6 +124,8 @@ RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorizati
context 'when user account is not confirmed' do
before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
+
user.update!(confirmed_at: nil)
request_oauth_token(user, client_basic_auth_header(client))
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index fdc25ecdcd3..3e7837866ae 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -35,39 +35,39 @@ RSpec.describe "Internal Project Pages Access", feature_category: :pages do
describe "GET /projects/:id/pages_access" do
context 'access depends on the level' do
- where(:pages_access_level, :with_user, :expected_result) do
- ProjectFeature::DISABLED | "admin" | 403
- ProjectFeature::DISABLED | "owner" | 403
- ProjectFeature::DISABLED | "master" | 403
- ProjectFeature::DISABLED | "developer" | 403
- ProjectFeature::DISABLED | "reporter" | 403
- ProjectFeature::DISABLED | "guest" | 403
- ProjectFeature::DISABLED | "user" | 403
- ProjectFeature::DISABLED | nil | 404
- ProjectFeature::PUBLIC | "admin" | 200
- ProjectFeature::PUBLIC | "owner" | 200
- ProjectFeature::PUBLIC | "master" | 200
- ProjectFeature::PUBLIC | "developer" | 200
- ProjectFeature::PUBLIC | "reporter" | 200
- ProjectFeature::PUBLIC | "guest" | 200
- ProjectFeature::PUBLIC | "user" | 200
- ProjectFeature::PUBLIC | nil | 404
- ProjectFeature::ENABLED | "admin" | 200
- ProjectFeature::ENABLED | "owner" | 200
- ProjectFeature::ENABLED | "master" | 200
- ProjectFeature::ENABLED | "developer" | 200
- ProjectFeature::ENABLED | "reporter" | 200
- ProjectFeature::ENABLED | "guest" | 200
- ProjectFeature::ENABLED | "user" | 200
- ProjectFeature::ENABLED | nil | 404
- ProjectFeature::PRIVATE | "admin" | 200
- ProjectFeature::PRIVATE | "owner" | 200
- ProjectFeature::PRIVATE | "master" | 200
- ProjectFeature::PRIVATE | "developer" | 200
- ProjectFeature::PRIVATE | "reporter" | 200
- ProjectFeature::PRIVATE | "guest" | 200
- ProjectFeature::PRIVATE | "user" | 403
- ProjectFeature::PRIVATE | nil | 404
+ where(:pages_access_level, :with_user, :admin_mode, :expected_result) do
+ ProjectFeature::DISABLED | "admin" | true | 403
+ ProjectFeature::DISABLED | "owner" | false | 403
+ ProjectFeature::DISABLED | "master" | false | 403
+ ProjectFeature::DISABLED | "developer" | false | 403
+ ProjectFeature::DISABLED | "reporter" | false | 403
+ ProjectFeature::DISABLED | "guest" | false | 403
+ ProjectFeature::DISABLED | "user" | false | 403
+ ProjectFeature::DISABLED | nil | false | 404
+ ProjectFeature::PUBLIC | "admin" | false | 200
+ ProjectFeature::PUBLIC | "owner" | false | 200
+ ProjectFeature::PUBLIC | "master" | false | 200
+ ProjectFeature::PUBLIC | "developer" | false | 200
+ ProjectFeature::PUBLIC | "reporter" | false | 200
+ ProjectFeature::PUBLIC | "guest" | false | 200
+ ProjectFeature::PUBLIC | "user" | false | 200
+ ProjectFeature::PUBLIC | nil | false | 404
+ ProjectFeature::ENABLED | "admin" | false | 200
+ ProjectFeature::ENABLED | "owner" | false | 200
+ ProjectFeature::ENABLED | "master" | false | 200
+ ProjectFeature::ENABLED | "developer" | false | 200
+ ProjectFeature::ENABLED | "reporter" | false | 200
+ ProjectFeature::ENABLED | "guest" | false | 200
+ ProjectFeature::ENABLED | "user" | false | 200
+ ProjectFeature::ENABLED | nil | false | 404
+ ProjectFeature::PRIVATE | "admin" | true | 200
+ ProjectFeature::PRIVATE | "owner" | false | 200
+ ProjectFeature::PRIVATE | "master" | false | 200
+ ProjectFeature::PRIVATE | "developer" | false | 200
+ ProjectFeature::PRIVATE | "reporter" | false | 200
+ ProjectFeature::PRIVATE | "guest" | false | 200
+ ProjectFeature::PRIVATE | "user" | false | 403
+ ProjectFeature::PRIVATE | nil | false | 404
end
with_them do
@@ -77,7 +77,7 @@ RSpec.describe "Internal Project Pages Access", feature_category: :pages do
it "correct return value" do
if !with_user.nil?
user = public_send(with_user)
- get api("/projects/#{project.id}/pages_access", user)
+ get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode)
else
get api("/projects/#{project.id}/pages_access")
end
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index c426f2a433c..0f6675799ad 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe API::Pages, feature_category: :pages do
- let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) }
+ let_it_be_with_reload(:project) { create(:project, path: 'my.project', pages_https_only: false) }
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
@@ -19,7 +19,7 @@ RSpec.describe API::Pages, feature_category: :pages do
end
it_behaves_like '404 response' do
- let(:request) { delete api("/projects/#{project.id}/pages", admin) }
+ let(:request) { delete api("/projects/#{project.id}/pages", admin, admin_mode: true) }
end
end
@@ -30,13 +30,13 @@ RSpec.describe API::Pages, feature_category: :pages do
context 'when Pages are deployed' do
it 'returns 204' do
- delete api("/projects/#{project.id}/pages", admin)
+ delete api("/projects/#{project.id}/pages", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end
it 'removes the pages' do
- delete api("/projects/#{project.id}/pages", admin)
+ delete api("/projects/#{project.id}/pages", admin, admin_mode: true)
expect(project.reload.pages_metadatum.deployed?).to be(false)
end
@@ -48,7 +48,7 @@ RSpec.describe API::Pages, feature_category: :pages do
end
it 'returns 204' do
- delete api("/projects/#{project.id}/pages", admin)
+ delete api("/projects/#{project.id}/pages", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end
@@ -58,7 +58,7 @@ RSpec.describe API::Pages, feature_category: :pages do
it 'returns 404' do
id = -1
- delete api("/projects/#{id}/pages", admin)
+ delete api("/projects/#{id}/pages", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index 5cc1b8f9a69..602eff73b0a 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -35,39 +35,39 @@ RSpec.describe "Private Project Pages Access", feature_category: :pages do
describe "GET /projects/:id/pages_access" do
context 'access depends on the level' do
- where(:pages_access_level, :with_user, :expected_result) do
- ProjectFeature::DISABLED | "admin" | 403
- ProjectFeature::DISABLED | "owner" | 403
- ProjectFeature::DISABLED | "master" | 403
- ProjectFeature::DISABLED | "developer" | 403
- ProjectFeature::DISABLED | "reporter" | 403
- ProjectFeature::DISABLED | "guest" | 403
- ProjectFeature::DISABLED | "user" | 404
- ProjectFeature::DISABLED | nil | 404
- ProjectFeature::PUBLIC | "admin" | 200
- ProjectFeature::PUBLIC | "owner" | 200
- ProjectFeature::PUBLIC | "master" | 200
- ProjectFeature::PUBLIC | "developer" | 200
- ProjectFeature::PUBLIC | "reporter" | 200
- ProjectFeature::PUBLIC | "guest" | 200
- ProjectFeature::PUBLIC | "user" | 404
- ProjectFeature::PUBLIC | nil | 404
- ProjectFeature::ENABLED | "admin" | 200
- ProjectFeature::ENABLED | "owner" | 200
- ProjectFeature::ENABLED | "master" | 200
- ProjectFeature::ENABLED | "developer" | 200
- ProjectFeature::ENABLED | "reporter" | 200
- ProjectFeature::ENABLED | "guest" | 200
- ProjectFeature::ENABLED | "user" | 404
- ProjectFeature::ENABLED | nil | 404
- ProjectFeature::PRIVATE | "admin" | 200
- ProjectFeature::PRIVATE | "owner" | 200
- ProjectFeature::PRIVATE | "master" | 200
- ProjectFeature::PRIVATE | "developer" | 200
- ProjectFeature::PRIVATE | "reporter" | 200
- ProjectFeature::PRIVATE | "guest" | 200
- ProjectFeature::PRIVATE | "user" | 404
- ProjectFeature::PRIVATE | nil | 404
+ where(:pages_access_level, :with_user, :admin_mode, :expected_result) do
+ ProjectFeature::DISABLED | "admin" | true | 403
+ ProjectFeature::DISABLED | "owner" | false | 403
+ ProjectFeature::DISABLED | "master" | false | 403
+ ProjectFeature::DISABLED | "developer" | false | 403
+ ProjectFeature::DISABLED | "reporter" | false | 403
+ ProjectFeature::DISABLED | "guest" | false | 403
+ ProjectFeature::DISABLED | "user" | false | 404
+ ProjectFeature::DISABLED | nil | false | 404
+ ProjectFeature::PUBLIC | "admin" | true | 200
+ ProjectFeature::PUBLIC | "owner" | false | 200
+ ProjectFeature::PUBLIC | "master" | false | 200
+ ProjectFeature::PUBLIC | "developer" | false | 200
+ ProjectFeature::PUBLIC | "reporter" | false | 200
+ ProjectFeature::PUBLIC | "guest" | false | 200
+ ProjectFeature::PUBLIC | "user" | false | 404
+ ProjectFeature::PUBLIC | nil | false | 404
+ ProjectFeature::ENABLED | "admin" | true | 200
+ ProjectFeature::ENABLED | "owner" | false | 200
+ ProjectFeature::ENABLED | "master" | false | 200
+ ProjectFeature::ENABLED | "developer" | false | 200
+ ProjectFeature::ENABLED | "reporter" | false | 200
+ ProjectFeature::ENABLED | "guest" | false | 200
+ ProjectFeature::ENABLED | "user" | false | 404
+ ProjectFeature::ENABLED | nil | false | 404
+ ProjectFeature::PRIVATE | "admin" | true | 200
+ ProjectFeature::PRIVATE | "owner" | false | 200
+ ProjectFeature::PRIVATE | "master" | false | 200
+ ProjectFeature::PRIVATE | "developer" | false | 200
+ ProjectFeature::PRIVATE | "reporter" | false | 200
+ ProjectFeature::PRIVATE | "guest" | false | 200
+ ProjectFeature::PRIVATE | "user" | false | 404
+ ProjectFeature::PRIVATE | nil | false | 404
end
with_them do
@@ -77,7 +77,7 @@ RSpec.describe "Private Project Pages Access", feature_category: :pages do
it "correct return value" do
if !with_user.nil?
user = public_send(with_user)
- get api("/projects/#{project.id}/pages_access", user)
+ get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode)
else
get api("/projects/#{project.id}/pages_access")
end
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index 1137f91f4b0..8b0ed7c59ab 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -35,39 +35,39 @@ RSpec.describe "Public Project Pages Access", feature_category: :pages do
describe "GET /projects/:id/pages_access" do
context 'access depends on the level' do
- where(:pages_access_level, :with_user, :expected_result) do
- ProjectFeature::DISABLED | "admin" | 403
- ProjectFeature::DISABLED | "owner" | 403
- ProjectFeature::DISABLED | "master" | 403
- ProjectFeature::DISABLED | "developer" | 403
- ProjectFeature::DISABLED | "reporter" | 403
- ProjectFeature::DISABLED | "guest" | 403
- ProjectFeature::DISABLED | "user" | 403
- ProjectFeature::DISABLED | nil | 403
- ProjectFeature::PUBLIC | "admin" | 200
- ProjectFeature::PUBLIC | "owner" | 200
- ProjectFeature::PUBLIC | "master" | 200
- ProjectFeature::PUBLIC | "developer" | 200
- ProjectFeature::PUBLIC | "reporter" | 200
- ProjectFeature::PUBLIC | "guest" | 200
- ProjectFeature::PUBLIC | "user" | 200
- ProjectFeature::PUBLIC | nil | 200
- ProjectFeature::ENABLED | "admin" | 200
- ProjectFeature::ENABLED | "owner" | 200
- ProjectFeature::ENABLED | "master" | 200
- ProjectFeature::ENABLED | "developer" | 200
- ProjectFeature::ENABLED | "reporter" | 200
- ProjectFeature::ENABLED | "guest" | 200
- ProjectFeature::ENABLED | "user" | 200
- ProjectFeature::ENABLED | nil | 200
- ProjectFeature::PRIVATE | "admin" | 200
- ProjectFeature::PRIVATE | "owner" | 200
- ProjectFeature::PRIVATE | "master" | 200
- ProjectFeature::PRIVATE | "developer" | 200
- ProjectFeature::PRIVATE | "reporter" | 200
- ProjectFeature::PRIVATE | "guest" | 200
- ProjectFeature::PRIVATE | "user" | 403
- ProjectFeature::PRIVATE | nil | 403
+ where(:pages_access_level, :with_user, :admin_mode, :expected_result) do
+ ProjectFeature::DISABLED | "admin" | false | 403
+ ProjectFeature::DISABLED | "owner" | false | 403
+ ProjectFeature::DISABLED | "master" | false | 403
+ ProjectFeature::DISABLED | "developer" | false | 403
+ ProjectFeature::DISABLED | "reporter" | false | 403
+ ProjectFeature::DISABLED | "guest" | false | 403
+ ProjectFeature::DISABLED | "user" | false | 403
+ ProjectFeature::DISABLED | nil | false | 403
+ ProjectFeature::PUBLIC | "admin" | false | 200
+ ProjectFeature::PUBLIC | "owner" | false | 200
+ ProjectFeature::PUBLIC | "master" | false | 200
+ ProjectFeature::PUBLIC | "developer" | false | 200
+ ProjectFeature::PUBLIC | "reporter" | false | 200
+ ProjectFeature::PUBLIC | "guest" | false | 200
+ ProjectFeature::PUBLIC | "user" | false | 200
+ ProjectFeature::PUBLIC | nil | false | 200
+ ProjectFeature::ENABLED | "admin" | false | 200
+ ProjectFeature::ENABLED | "owner" | false | 200
+ ProjectFeature::ENABLED | "master" | false | 200
+ ProjectFeature::ENABLED | "developer" | false | 200
+ ProjectFeature::ENABLED | "reporter" | false | 200
+ ProjectFeature::ENABLED | "guest" | false | 200
+ ProjectFeature::ENABLED | "user" | false | 200
+ ProjectFeature::ENABLED | nil | false | 200
+ ProjectFeature::PRIVATE | "admin" | true | 200
+ ProjectFeature::PRIVATE | "owner" | false | 200
+ ProjectFeature::PRIVATE | "master" | false | 200
+ ProjectFeature::PRIVATE | "developer" | false | 200
+ ProjectFeature::PRIVATE | "reporter" | false | 200
+ ProjectFeature::PRIVATE | "guest" | false | 200
+ ProjectFeature::PRIVATE | "user" | false | 403
+ ProjectFeature::PRIVATE | nil | false | 403
end
with_them do
@@ -77,7 +77,7 @@ RSpec.describe "Public Project Pages Access", feature_category: :pages do
it "correct return value" do
if !with_user.nil?
user = public_send(with_user)
- get api("/projects/#{project.id}/pages_access", user)
+ get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode)
else
get api("/projects/#{project.id}/pages_access")
end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index ba1fb5105b8..ea83fa384af 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -41,14 +41,14 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
end
it_behaves_like '404 response' do
- let(:request) { get api('/pages/domains', admin) }
+ let(:request) { get api('/pages/domains', admin, admin_mode: true) }
end
end
context 'when pages is enabled' do
context 'when authenticated as an admin' do
- it 'returns paginated all pages domains' do
- get api('/pages/domains', admin)
+ it 'returns paginated all pages domains', :aggregate_failures do
+ get api('/pages/domains', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
@@ -74,7 +74,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
- it 'returns paginated pages domains' do
+ it 'returns paginated pages domains', :aggregate_failures do
get api(route, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -145,7 +145,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
describe 'GET /projects/:project_id/pages/domains/:domain' do
shared_examples_for 'get pages domain' do
- it 'returns pages domain' do
+ it 'returns pages domain', :aggregate_failures do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -155,7 +155,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['certificate']).to be_nil
end
- it 'returns pages domain with project path' do
+ it 'returns pages domain with project path', :aggregate_failures do
get api(route_domain_path, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -165,7 +165,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['certificate']).to be_nil
end
- it 'returns pages domain with a certificate' do
+ it 'returns pages domain with a certificate', :aggregate_failures do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -177,7 +177,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['auto_ssl_enabled']).to be false
end
- it 'returns pages domain with an expired certificate' do
+ it 'returns pages domain with an expired certificate', :aggregate_failures do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -185,7 +185,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['certificate']['expired']).to be true
end
- it 'returns pages domain with letsencrypt' do
+ it 'returns pages domain with letsencrypt', :aggregate_failures do
get api(route_letsencrypt_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -258,7 +258,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) }
shared_examples_for 'post pages domains' do
- it 'creates a new pages domain' do
+ it 'creates a new pages domain', :aggregate_failures do
expect { post api(route, user), params: params }
.to publish_event(PagesDomains::PagesDomainCreatedEvent)
.with(
@@ -279,7 +279,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be false
end
- it 'creates a new secure pages domain' do
+ it 'creates a new secure pages domain', :aggregate_failures do
post api(route, user), params: params_secure
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
@@ -291,7 +291,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be false
end
- it 'creates domain with letsencrypt enabled' do
+ it 'creates domain with letsencrypt enabled', :aggregate_failures do
post api(route, user), params: pages_domain_with_letsencrypt_params
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
@@ -301,7 +301,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be true
end
- it 'creates domain with letsencrypt enabled and provided certificate' do
+ it 'creates domain with letsencrypt enabled and provided certificate', :aggregate_failures do
post api(route, user), params: params_secure.merge(auto_ssl_enabled: true)
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
@@ -376,7 +376,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) }
shared_examples_for 'put pages domain' do
- it 'updates pages domain removing certificate' do
+ it 'updates pages domain removing certificate', :aggregate_failures do
put api(route_secure_domain, user), params: { certificate: nil, key: nil }
pages_domain_secure.reload
@@ -399,7 +399,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
)
end
- it 'updates pages domain adding certificate' do
+ it 'updates pages domain adding certificate', :aggregate_failures do
put api(route_domain, user), params: params_secure
pages_domain.reload
@@ -409,7 +409,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.key).to eq(params_secure[:key])
end
- it 'updates pages domain adding certificate with letsencrypt' do
+ it 'updates pages domain adding certificate with letsencrypt', :aggregate_failures do
put api(route_domain, user), params: params_secure.merge(auto_ssl_enabled: true)
pages_domain.reload
@@ -420,7 +420,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be true
end
- it 'updates pages domain enabling letsencrypt' do
+ it 'updates pages domain enabling letsencrypt', :aggregate_failures do
put api(route_domain, user), params: { auto_ssl_enabled: true }
pages_domain.reload
@@ -429,7 +429,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be true
end
- it 'updates pages domain disabling letsencrypt while preserving the certificate' do
+ it 'updates pages domain disabling letsencrypt while preserving the certificate', :aggregate_failures do
put api(route_letsencrypt_domain, user), params: { auto_ssl_enabled: false }
pages_domain_with_letsencrypt.reload
@@ -440,7 +440,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain_with_letsencrypt.certificate).to be
end
- it 'updates pages domain with expired certificate' do
+ it 'updates pages domain with expired certificate', :aggregate_failures do
put api(route_expired_domain, user), params: params_secure
pages_domain_expired.reload
@@ -450,7 +450,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
- it 'updates pages domain with expired certificate not updating key' do
+ it 'updates pages domain with expired certificate not updating key', :aggregate_failures do
put api(route_secure_domain, user), params: params_secure_nokey
pages_domain_secure.reload
diff --git a/spec/requests/api/personal_access_tokens/self_information_spec.rb b/spec/requests/api/personal_access_tokens/self_information_spec.rb
index 4a3c0ad8904..2a7af350054 100644
--- a/spec/requests/api/personal_access_tokens/self_information_spec.rb
+++ b/spec/requests/api/personal_access_tokens/self_information_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :authentication_and_authorization do
+RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :system_access do
let(:path) { '/personal_access_tokens/self' }
let(:token) { create(:personal_access_token, user: current_user) }
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 32adc7ebd61..cca94c7a012 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_authorization do
+RSpec.describe API::PersonalAccessTokens, feature_category: :system_access do
let_it_be(:path) { '/personal_access_tokens' }
describe 'GET /personal_access_tokens' do
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 9d722e4a445..978ac28ef73 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -6,8 +6,12 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) }
let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
- let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
let_it_be(:route) { "/projects/#{project.id}/milestones" }
+ let_it_be(:milestone) do
+ create(:milestone, project: project, title: 'version2', description: 'open milestone', updated_at: 5.days.ago)
+ end
+
+ let(:params) { {} }
before_all do
project.add_reporter(user)
@@ -15,38 +19,43 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
it_behaves_like 'group and project milestones', "/projects/:id/milestones"
+ shared_examples 'listing all milestones' do
+ it 'returns correct list of milestones' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(milestones.size)
+ expect(json_response.map { |entry| entry["id"] }).to match_array(milestones.map(&:id))
+ end
+ end
+
describe 'GET /projects/:id/milestones' do
- context 'when include_parent_milestones is true' do
- let_it_be(:ancestor_group) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: ancestor_group) }
- let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) }
- let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:ancestor_group) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: ancestor_group) }
+ let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 1.day.ago) }
+ let_it_be(:group_milestone) { create(:milestone, group: group, updated_at: 3.days.ago) }
- let(:params) { { include_parent_milestones: true } }
+ context 'when project parent is a namespace' do
+ let(:milestones) { [milestone, closed_milestone] }
- shared_examples 'listing all milestones' do
- it 'returns correct list of milestones' do
- get api(route, user), params: params
+ it_behaves_like 'listing all milestones'
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(milestones.size)
- expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
- end
+ context 'when include_parent_milestones is true' do
+ let(:params) { { include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones'
end
+ end
- context 'when project parent is a namespace' do
- it_behaves_like 'listing all milestones' do
- let(:milestones) { [milestone, closed_milestone] }
- end
+ context 'when project parent is a group' do
+ before_all do
+ project.update!(namespace: group)
end
- context 'when project parent is a group' do
+ context 'when include_parent_milestones is true' do
+ let(:params) { { include_parent_milestones: true } }
let(:milestones) { [group_milestone, ancestor_group_milestone, milestone, closed_milestone] }
- before_all do
- project.update!(namespace: group)
- end
-
it_behaves_like 'listing all milestones'
context 'when iids param is present' do
@@ -64,6 +73,38 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 12.hours.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [group_milestone, ancestor_group_milestone, milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 2.days.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [ancestor_group_milestone, closed_milestone] }
+ end
+ end
+ end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 12.hours.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 2.days.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [closed_milestone] }
+ end
end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e78ef2f7630..d755a4231da 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1109,6 +1109,66 @@ RSpec.describe API::Projects, feature_category: :projects do
end.not_to exceed_query_limit(control)
end
end
+
+ context 'rate limiting' do
+ let_it_be(:current_user) { create(:user) }
+
+ shared_examples_for 'does not log request and does not block the request' do
+ specify do
+ request
+ request
+
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+ end
+ end
+
+ before do
+ stub_application_setting(projects_api_rate_limit_unauthenticated: 1)
+ end
+
+ context 'when the user is signed in' do
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+
+ context 'when the user is not signed in' do
+ let_it_be(:current_user) { nil }
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :projects_api_rate_limit_unauthenticated do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+
+ context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is disabled' do
+ before do
+ stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
+ end
+
+ context 'when the user is not signed in' do
+ let_it_be(:current_user) { nil }
+
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+
+ context 'when the user is signed in' do
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api('/projects', current_user)
+ end
+ end
+ end
+ end
+ end
end
describe 'POST /projects' do
@@ -2006,19 +2066,6 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
- context 'with upload size enforcement disabled' do
- before do
- stub_feature_flags(enforce_max_attachment_size_upload_api: false)
- end
-
- it "returns 200" do
- post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['MaximumSize']).to eq(1.gigabyte)
- end
- end
-
context 'with no Workhorse headers' do
it "returns 403" do
post api("/projects/#{project.id}/uploads/authorize", user)
@@ -2095,14 +2142,6 @@ RSpec.describe API::Projects, feature_category: :projects do
it_behaves_like 'capped upload attachments', true
end
-
- context 'with upload size enforcement disabled' do
- before do
- stub_feature_flags(enforce_max_attachment_size_upload_api: false)
- end
-
- it_behaves_like 'capped upload attachments', false
- end
end
describe "GET /projects/:id/groups" do
@@ -2228,9 +2267,9 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /project/:id/share_locations' do
- let_it_be(:root_group) { create(:group, :public, name: 'root group') }
- let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1') }
- let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2') }
+ let_it_be(:root_group) { create(:group, :public, name: 'root group', path: 'root-group-path') }
+ let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1', path: 'group-1-path') }
+ let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2', path: 'group-2-path') }
let_it_be(:project) { create(:project, :private, group: project_group1) }
shared_examples_for 'successful groups response' do
@@ -2280,10 +2319,22 @@ RSpec.describe API::Projects, feature_category: :projects do
end
context 'when searching by group name' do
- let(:params) { { search: 'group1' } }
+ context 'searching by group name' do
+ it_behaves_like 'successful groups response' do
+ let(:params) { { search: 'group1' } }
+ let(:expected_groups) { [project_group1] }
+ end
+ end
- it_behaves_like 'successful groups response' do
- let(:expected_groups) { [project_group1] }
+ context 'searching by full group path' do
+ let_it_be(:project_group2_subgroup) do
+ create(:group, :public, parent: project_group2, name: 'subgroup', path: 'subgroup-path')
+ end
+
+ it_behaves_like 'successful groups response' do
+ let(:params) { { search: 'root-group-path/group-2-path/subgroup-path' } }
+ let(:expected_groups) { [project_group2_subgroup] }
+ end
end
end
end
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 8e8a25a8dc2..463893afd13 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -266,6 +266,15 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
end.to change { protected_branch.reload.allow_force_push }.from(false).to(true)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when allow_force_push is not set' do
+ it 'responds with a bad request error' do
+ patch api(route, user), params: { allow_force_push: nil }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'allow_force_push is empty'
+ end
+ end
end
context 'when returned protected branch is invalid' do
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 978d4f72a4a..0b2641b062c 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -14,10 +14,18 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
let(:headers) { {} }
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user }
+ end
+ end
+
context 'simple index API endpoint' do
let_it_be(:package) { create(:pypi_package, project: project) }
let_it_be(:package2) { create(:pypi_package, project: project) }
@@ -26,7 +34,6 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple' do
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple index API endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -82,13 +89,13 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
context 'simple package API endpoint' do
let_it_be(:package) { create(:pypi_package, project: project) }
- let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group, property: 'i_package_pypi_user' } }
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do
let(:package_name) { package.name }
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package_name}" }
+ let(:snowplow_context) { { project: nil, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple API endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -126,7 +133,7 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
let(:package_name) { package.name }
let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package_name}" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
+ let(:snowplow_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple API endpoint'
it_behaves_like 'rejects PyPI access with unknown project id'
@@ -242,6 +249,13 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) do
+ if user_role == :anonymous || (visibility_level == :public && !user_token)
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user }
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
@@ -379,6 +393,14 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let_it_be(:package_name) { 'Dummy-Package' }
let_it_be(:package) { create(:pypi_package, project: project, name: package_name, version: '1.0.0') }
+ let(:snowplow_gitlab_standard_context) do
+ if user_role == :anonymous || (visibility_level == :public && !user_token)
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user }
+ end
+ end
+
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/files/:sha256/*file_identifier' do
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 462cc1e3b5d..4b388304621 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -377,6 +377,15 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
expect(response).to match_response_schema('release/link')
end
+ context 'when params are invalid' do
+ it 'returns 400 error' do
+ put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer),
+ params: params.merge(url: 'wrong_url')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when using `direct_asset_path`' do
it 'updates the release link' do
put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer),
@@ -534,6 +543,21 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
end
+ context 'when destroy process fails' do
+ before do
+ allow_next_instance_of(::Releases::Links::DestroyService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it_behaves_like '400 response' do
+ let(:message) { 'error' }
+ let(:request) do
+ delete api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer)
+ end
+ end
+ end
+
context 'when there are no corresponding release link' do
let!(:release_link) {}
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 555ba2bc978..b146dda5030 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -236,7 +236,6 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
get api(route, current_user)
expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
- expect(response.headers["Pragma"]).to eq("no-cache")
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 6a89e9a56df..9277fa18219 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_authorization do
+RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:user_non_priviledged) { create(:user) }
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index 34cf6033811..1774b43ccb3 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -8,6 +8,14 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
+ let(:tokens) do
+ {
+ personal_access_token: personal_access_token.token,
+ deploy_token: deploy_token.token,
+ job_token: job.token
+ }
+ end
+
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
@@ -15,14 +23,14 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:headers) { {} }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_rubygems_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
- let(:tokens) do
- {
- personal_access_token: personal_access_token.token,
- deploy_token: deploy_token.token,
- job_token: job.token
- }
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_rubygems_user', user: user }
+ end
end
shared_examples 'when feature flag is disabled' do
@@ -164,7 +172,13 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' } }
+ let(:snowplow_gitlab_standard_context) do
+ if token_type == :deploy_token
+ snowplow_context.merge(user: deploy_token)
+ else
+ snowplow_context(user_role: user_role)
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 035f53db12e..eb0f3b3eaee 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -174,6 +174,23 @@ RSpec.describe API::Search, feature_category: :global_search do
end
end
+ context 'when there is a search error' do
+ let(:results) { instance_double('Gitlab::SearchResults', failed?: true, error: 'failed to parse query') }
+
+ before do
+ allow_next_instance_of(SearchService) do |service|
+ allow(service).to receive(:search_objects).and_return([])
+ allow(service).to receive(:search_results).and_return(results)
+ end
+ end
+
+ it 'returns 400 error' do
+ get api(endpoint, user), params: { scope: 'issues', search: 'expected to fail' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'with correct params' do
context 'for projects scope' do
before do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 4d85849cff3..e91d777bfb0 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :not_owned do
+RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :shared do
let(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
@@ -66,6 +66,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['jira_connect_application_key']).to eq(nil)
expect(json_response['jira_connect_proxy_url']).to eq(nil)
expect(json_response['user_defaults_to_private_profile']).to eq(false)
+ expect(json_response['default_syntax_highlighting_theme']).to eq(1)
+ expect(json_response['projects_api_rate_limit_unauthenticated']).to eq(400)
end
end
@@ -169,7 +171,9 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
jira_connect_proxy_url: 'http://example.com',
bulk_import_enabled: false,
allow_runner_registration_token: true,
- user_defaults_to_private_profile: true
+ user_defaults_to_private_profile: true,
+ default_syntax_highlighting_theme: 2,
+ projects_api_rate_limit_unauthenticated: 100
}
expect(response).to have_gitlab_http_status(:ok)
@@ -237,6 +241,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['bulk_import_enabled']).to be(false)
expect(json_response['allow_runner_registration_token']).to be(true)
expect(json_response['user_defaults_to_private_profile']).to be(true)
+ expect(json_response['default_syntax_highlighting_theme']).to eq(2)
+ expect(json_response['projects_api_rate_limit_unauthenticated']).to be(100)
end
end
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 1085df97cc7..32c4c323923 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do
+RSpec.describe API::SidekiqMetrics, feature_category: :shared do
let(:admin) { create(:user, :admin) }
describe 'GET sidekiq/*' do
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index 2bd7cb027aa..f479ca25f3c 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -415,12 +415,15 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
with_them do
let(:snowplow_gitlab_standard_context) do
- {
+ context = {
project: project,
- user: user_role == :anonymous ? nil : user,
namespace: project.namespace,
property: 'i_package_terraform_module_user'
}
+
+ context[:user] = user if user_role != :anonymous
+
+ context
end
before do
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index fd34345d814..c94643242c9 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
before do
stub_terraform_state_object_storage
+ stub_config(terraform_state: { enabled: true })
end
shared_examples 'endpoint with unique user tracking' do
@@ -51,7 +52,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject(:api_request) { request }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'terraform_state_api_request' }
let(:label) { 'redis_hll_counters.terraform.p_terraform_state_api_unique_users_monthly' }
@@ -82,6 +82,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
subject(:request) { get api(state_path), headers: auth_header }
it_behaves_like 'endpoint with unique user tracking'
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
context 'without authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
@@ -194,6 +195,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params }
it_behaves_like 'endpoint with unique user tracking'
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
context 'when terraform state with a given name is already present' do
context 'with maintainer permissions' do
@@ -372,6 +374,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
subject(:request) { delete api(state_path), headers: auth_header }
it_behaves_like 'endpoint with unique user tracking'
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
shared_examples 'schedules the state for deletion' do
it 'returns empty body' do
@@ -553,6 +556,10 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
let(:lock_id) { 'irrelevant to this test, just needs to be present' }
end
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config' do
+ let(:lock_id) { '123.456' }
+ end
+
where(given_state_name: %w[test-state test.state test%2Ffoo])
with_them do
let(:state_name) { given_state_name }
diff --git a/spec/requests/api/terraform/state_version_spec.rb b/spec/requests/api/terraform/state_version_spec.rb
index 28abbb5749d..24b3ca94581 100644
--- a/spec/requests/api/terraform/state_version_spec.rb
+++ b/spec/requests/api/terraform/state_version_spec.rb
@@ -22,9 +22,15 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a
let(:version_serial) { version.version }
let(:state_version_path) { "/projects/#{project_id}/terraform/state/#{state_name}/versions/#{version_serial}" }
+ before do
+ stub_config(terraform_state: { enabled: true })
+ end
+
describe 'GET /projects/:id/terraform/state/:name/versions/:serial' do
subject(:request) { get api(state_version_path), headers: auth_header }
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
+
context 'with invalid authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
@@ -147,6 +153,8 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a
describe 'DELETE /projects/:id/terraform/state/:name/versions/:serial' do
subject(:request) { delete api(state_version_path), headers: auth_header }
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config', { success_status: :no_content }
+
context 'with invalid authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 5daf7cd7b75..75b26b98228 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -88,6 +88,14 @@ RSpec.describe API::Unleash, feature_category: :feature_flags do
end
end
+ describe 'GET /feature_flags/unleash/:project_id/client/features', :use_clean_rails_redis_caching do
+ specify do
+ get api("/feature_flags/unleash/#{project_id}/client/features"), params: params, headers: headers
+
+ is_expected.to have_request_urgency(:medium)
+ end
+ end
+
%w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 34867b13db2..c924f529e11 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1288,7 +1288,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
expect(json_response['message']['projects_limit'])
.to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username'])
- .to eq([Gitlab::PathRegex.namespace_format_message])
+ .to match_array([Gitlab::PathRegex.namespace_format_message, Gitlab::Regex.oci_repository_path_regex_message])
end
it 'tracks weak password errors' do
@@ -1823,7 +1823,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
expect(json_response['message']['projects_limit'])
.to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username'])
- .to eq([Gitlab::PathRegex.namespace_format_message])
+ .to match_array([Gitlab::PathRegex.namespace_format_message, Gitlab::Regex.oci_repository_path_regex_message])
end
it 'returns 400 if provider is missing for identity update' do
@@ -4150,7 +4150,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status).to be_nil
+ expect(user_with_status.reset.status).to be_nil
end
end
end
@@ -4178,7 +4178,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status.clear_status_at).to be_nil
+ expect(user_with_status.reset.status.clear_status_at).to be_nil
end
end
@@ -4217,7 +4217,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status).to be_nil
+ expect(user_with_status.reset.status).to be_nil
end
end
@@ -4229,7 +4229,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status.clear_status_at).to be_nil
+ expect(user_with_status.reset.status.clear_status_at).to be_nil
end
end
end
diff --git a/spec/requests/dashboard_controller_spec.rb b/spec/requests/dashboard_controller_spec.rb
index 1c8ab843ebe..d7f01b8a7ab 100644
--- a/spec/requests/dashboard_controller_spec.rb
+++ b/spec/requests/dashboard_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DashboardController, feature_category: :authentication_and_authorization do
+RSpec.describe DashboardController, feature_category: :system_access do
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'issues atom', public_resource: false do
let(:url) { issues_dashboard_url(:atom, assignee_username: user.username) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 02b99eba8ce..d3d1a2a6cd0 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -230,6 +230,12 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
context 'when authenticated' do
it 'creates a new project under the existing namespace' do
+ # current scenario does not matter with the user activity case,
+ # so stub/double it to escape more sql running times limit
+ activity_service = instance_double(::Users::ActivityService)
+ allow(::Users::ActivityService).to receive(:new).and_return(activity_service)
+ allow(activity_service).to receive(:execute)
+
expect do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
@@ -472,10 +478,11 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
end
context 'when the request is not from gitlab-workhorse' do
- it 'raises an exception' do
- expect do
- get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
- end.to raise_error(JWT::DecodeError)
+ it 'responds with 403 Forbidden' do
+ get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response.body).to eq('Nil JSON web token')
end
end
@@ -1112,10 +1119,11 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
end
context 'when the request is not from gitlab-workhorse' do
- it 'raises an exception' do
- expect do
- get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
- end.to raise_error(JWT::DecodeError)
+ it 'responds with 403 Forbidden' do
+ get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response.body).to eq('Nil JSON web token')
end
end
diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb
index 7db5c084793..b6e765eba37 100644
--- a/spec/requests/groups/email_campaigns_controller_spec.rb
+++ b/spec/requests/groups/email_campaigns_controller_spec.rb
@@ -38,11 +38,7 @@ RSpec.describe Groups::EmailCampaignsController, feature_category: :navigation d
expect(subject).to have_gitlab_http_status(:redirect)
end
- context 'on .com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'on SaaS', :saas do
it 'emits a snowplow event', :snowplow do
subject
diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb
index 471cad40c90..b82cf2b0bad 100644
--- a/spec/requests/groups/observability_controller_spec.rb
+++ b/spec/requests/groups/observability_controller_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
end
end
- context 'when user is not a developer' do
+ context 'when user is a guest' do
before do
sign_in(user)
end
@@ -36,10 +36,10 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
end
end
- context 'when user is authenticated and a developer' do
+ context 'when user has the correct permissions' do
before do
sign_in(user)
- group.add_developer(user)
+ set_permissions
end
context 'when observability url is missing' do
@@ -75,13 +75,21 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
let(:path) { group_observability_explore_path(group) }
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/explore" }
- it_behaves_like 'observability route request'
+ it_behaves_like 'observability route request' do
+ let(:set_permissions) do
+ group.add_developer(user)
+ end
+ end
end
describe 'GET #datasources' do
let(:path) { group_observability_datasources_path(group) }
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/datasources" }
- it_behaves_like 'observability route request'
+ it_behaves_like 'observability route request' do
+ let(:set_permissions) do
+ group.add_maintainer(user)
+ end
+ end
end
end
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
index f26b69f8d30..0204af8ea8e 100644
--- a/spec/requests/groups/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::AccessTokensController, feature_category: :authentication_and_authorization do
+RSpec.describe Groups::Settings::AccessTokensController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:resource) { create(:group) }
let_it_be(:access_token_user) { create(:user, :project_bot) }
diff --git a/spec/requests/groups/settings/applications_controller_spec.rb b/spec/requests/groups/settings/applications_controller_spec.rb
index fb91cd8bdab..2fcf80658b2 100644
--- a/spec/requests/groups/settings/applications_controller_spec.rb
+++ b/spec/requests/groups/settings/applications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::ApplicationsController, feature_category: :authentication_and_authorization do
+RSpec.describe Groups::Settings::ApplicationsController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index b287ded799d..38708399519 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -19,7 +19,6 @@ RSpec.describe IdeController, feature_category: :web_ide do
let_it_be(:top_nav_partial) { 'layouts/header/_default' }
let(:user) { creator }
- let(:branch) { '' }
def find_csp_frame_src
csp = response.headers['Content-Security-Policy']
@@ -42,14 +41,14 @@ RSpec.describe IdeController, feature_category: :web_ide do
subject { get route }
shared_examples 'user access rights check' do
- context 'user can read project' do
+ context 'when user can read project' do
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
subject
end
- context 'user can read project but cannot push code' do
+ context 'when user can read project but cannot push code' do
include ProjectForksHelper
let(:user) { reporter }
@@ -60,7 +59,15 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
+
+ expect(assigns(:fork_info)).to eq({
+ fork_path: controller.helpers.ide_fork_and_edit_path(
+ project,
+ '',
+ '',
+ with_notice: false
+ )
+ })
end
it 'has nil fork_info if user cannot fork' do
@@ -81,13 +88,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
+ expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, '', '') })
end
end
end
end
- context 'user cannot read project' do
+ context 'when user cannot read project' do
let(:user) { other_user }
it 'returns 404' do
@@ -98,7 +105,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide' do
+ context 'with /-/ide' do
let(:route) { '/-/ide' }
it 'returns 404' do
@@ -108,7 +115,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide/project' do
+ context 'with /-/ide/project' do
let(:route) { '/-/ide/project' }
it 'returns 404' do
@@ -118,7 +125,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide/project/:project' do
+ context 'with /-/ide/project/:project' do
let(:route) { "/-/ide/project/#{project.full_path}" }
it 'instantiates project instance var and returns 200' do
@@ -126,16 +133,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user access rights check'
- %w(edit blob tree).each do |action|
- context "/-/ide/project/:project/#{action}" do
+ %w[edit blob tree].each do |action|
+ context "with /-/ide/project/:project/#{action}" do
let(:route) { "/-/ide/project/#{project.full_path}/#{action}" }
it 'instantiates project instance var and returns 200' do
@@ -143,87 +147,11 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch" do
- let(:branch) { 'master' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}" }
-
- it 'instantiates project and branch instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch/-" do
- let(:branch) { 'branch/slash' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-" }
-
- it 'instantiates project and branch instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch/-/:path" do
- let(:branch) { 'master' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-/foo/.bar" }
-
- it 'instantiates project, branch, and path instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to eq 'foo/.bar'
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
- end
- end
- end
- end
- end
-
- context '/-/ide/project/:project/merge_requests/:merge_request_id' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
-
- let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" }
-
- it 'instantiates project and merge_request instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to eq merge_request.id.to_s
- expect(assigns(:fork_info)).to be_nil
end
-
- it_behaves_like 'user access rights check'
end
describe 'Snowplow view event', :snowplow do
@@ -237,18 +165,6 @@ RSpec.describe IdeController, feature_category: :web_ide do
user: user
)
end
-
- context 'when route_hll_to_snowplow_phase2 FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'does not track Snowplow event' do
- subject
-
- expect_no_snowplow_event
- end
- end
end
# This indirectly tests that `minimal: true` was passed to the fullscreen layout
diff --git a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
index d111edd06da..2f6113c6dd7 100644
--- a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
+++ b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
@@ -38,11 +38,7 @@ RSpec.describe JiraConnect::OauthApplicationIdsController, feature_category: :in
end
end
- context 'on GitLab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'on SaaS', :saas do
it 'renders not found' do
get '/-/jira_connect/oauth_application_id'
diff --git a/spec/requests/jira_connect/public_keys_controller_spec.rb b/spec/requests/jira_connect/public_keys_controller_spec.rb
index 7f0262eaf65..62a81d43e65 100644
--- a/spec/requests/jira_connect/public_keys_controller_spec.rb
+++ b/spec/requests/jira_connect/public_keys_controller_spec.rb
@@ -5,11 +5,10 @@ require 'spec_helper'
RSpec.describe JiraConnect::PublicKeysController, feature_category: :integrations do
describe 'GET /-/jira_connect/public_keys/:uuid' do
let(:uuid) { non_existing_record_id }
- let(:public_key_storage_enabled_config) { true }
+ let(:public_key_storage_enabled) { true }
before do
- allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
- .and_return(public_key_storage_enabled_config)
+ stub_application_setting(jira_connect_public_key_storage_enabled: public_key_storage_enabled)
end
it 'renders 404' do
@@ -30,26 +29,14 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration
expect(response.body).to eq(public_key.key)
end
- context 'when public key storage config disabled' do
- let(:public_key_storage_enabled_config) { false }
+ context 'when public key storage setting disabled' do
+ let(:public_key_storage_enabled) { false }
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
expect(response).to have_gitlab_http_status(:not_found)
end
-
- context 'when public key storage setting is enabled' do
- before do
- stub_application_setting(jira_connect_public_key_storage_enabled: true)
- end
-
- it 'renders 404' do
- get jira_connect_public_key_path(id: uuid)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
end
end
end
diff --git a/spec/requests/jwks_controller_spec.rb b/spec/requests/jwks_controller_spec.rb
index ac9765c35d8..c6f5f7c6bea 100644
--- a/spec/requests/jwks_controller_spec.rb
+++ b/spec/requests/jwks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JwksController, feature_category: :authentication_and_authorization do
+RSpec.describe JwksController, feature_category: :system_access do
describe 'Endpoints from the parent Doorkeeper::OpenidConnect::DiscoveryController' do
it 'respond successfully' do
[
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 00222cb1977..69127a7526e 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JwtController, feature_category: :authentication_and_authorization do
+RSpec.describe JwtController, feature_category: :system_access do
include_context 'parsed logs'
let(:service) { double(execute: {} ) }
@@ -53,6 +53,14 @@ RSpec.describe JwtController, feature_category: :authentication_and_authorizatio
end
end
+ context 'POST /jwt/auth' do
+ it 'returns 404' do
+ post '/jwt/auth'
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'authenticating against container registry' do
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
diff --git a/spec/requests/oauth/applications_controller_spec.rb b/spec/requests/oauth/applications_controller_spec.rb
index 94ee08f6272..8c2856b87d1 100644
--- a/spec/requests/oauth/applications_controller_spec.rb
+++ b/spec/requests/oauth/applications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::ApplicationsController, feature_category: :authentication_and_authorization do
+RSpec.describe Oauth::ApplicationsController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:application) { create(:oauth_application, owner: user) }
let_it_be(:show_path) { oauth_application_path(application) }
diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb
index 52188717210..257f238d9ef 100644
--- a/spec/requests/oauth/authorizations_controller_spec.rb
+++ b/spec/requests/oauth/authorizations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::AuthorizationsController, feature_category: :authentication_and_authorization do
+RSpec.describe Oauth::AuthorizationsController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:application) { create(:oauth_application, redirect_uri: 'custom://test') }
let_it_be(:oauth_authorization_path) do
diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb
index cdfad8cb59c..58203a81bac 100644
--- a/spec/requests/oauth/tokens_controller_spec.rb
+++ b/spec/requests/oauth/tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::TokensController, feature_category: :authentication_and_authorization do
+RSpec.describe Oauth::TokensController, feature_category: :system_access do
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
let(:other_headers) { {} }
let(:headers) { cors_request_headers.merge(other_headers) }
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
index 053bd317fcc..67c676fdb40 100644
--- a/spec/requests/oauth_tokens_spec.rb
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth Tokens requests', feature_category: :authentication_and_authorization do
+RSpec.describe 'OAuth Tokens requests', feature_category: :system_access do
let(:user) { create :user }
let(:application) { create :oauth_application, scopes: 'api' }
let(:grant_type) { 'authorization_code' }
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 9035e723abe..2e158190734 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_authorization do
+RSpec.describe 'OpenID Connect requests', feature_category: :system_access do
let(:user) do
create(
:user,
diff --git a/spec/requests/projects/airflow/dags_controller_spec.rb b/spec/requests/projects/airflow/dags_controller_spec.rb
deleted file mode 100644
index 2dcedf5f128..00000000000
--- a/spec/requests/projects/airflow/dags_controller_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::Airflow::DagsController, feature_category: :dataops do
- let_it_be(:non_member) { create(:user) }
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group).tap { |p| p.add_developer(user) } }
- let_it_be(:project) { create(:project, group: group).tap { |p| p.add_developer(user) } }
-
- let(:current_user) { user }
- let(:feature_flag) { true }
-
- let_it_be(:dags) do
- create_list(:airflow_dags, 5, project: project)
- end
-
- let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
- let(:extra_params) { {} }
-
- before do
- sign_in(current_user) if current_user
- stub_feature_flags(airflow_dags: false)
- stub_feature_flags(airflow_dags: project) if feature_flag
- list_dags
- end
-
- shared_examples 'returns a 404 if feature flag disabled' do
- context 'when :airflow_dags disabled' do
- let(:feature_flag) { false }
-
- it 'is 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET index' do
- it 'renders the template' do
- expect(response).to render_template('projects/airflow/dags/index')
- end
-
- describe 'pagination' do
- before do
- stub_const("Projects::Airflow::DagsController::MAX_DAGS_PER_PAGE", 2)
- dags
-
- list_dags
- end
-
- context 'when out of bounds' do
- let(:params) { extra_params.merge(page: 10000) }
-
- it 'redirects to last page' do
- last_page = (dags.size + 1) / 2
- expect(response).to redirect_to(project_airflow_dags_path(project, page: last_page))
- end
- end
-
- context 'when bad page' do
- let(:params) { extra_params.merge(page: 's') }
-
- it 'uses first page' do
- expect(assigns(:pagination)).to include(
- page: 1,
- is_last_page: false,
- per_page: 2,
- total_items: dags.size)
- end
- end
- end
-
- it 'does not perform N+1 sql queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_dags }
-
- create_list(:airflow_dags, 1, project: project)
-
- expect { list_dags }.not_to exceed_all_query_limit(control_count)
- end
-
- context 'when user is not logged in' do
- let(:current_user) { nil }
-
- it 'redirects to login' do
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'when user is not a member' do
- let(:current_user) { non_member }
-
- it 'returns a 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- it_behaves_like 'returns a 404 if feature flag disabled'
- end
-
- private
-
- def list_dags
- get project_airflow_dags_path(project), params: params
- end
-end
diff --git a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
index b0c7427fa81..11f962e0e96 100644
--- a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
+++ b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_authoring do
+RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :public) }
describe 'POST /*namespace_id/:project_id/-/ci/prometheus_metrics/histograms' do
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index d564a31f835..14214b8fdfb 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -108,66 +108,104 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :
end
end
- it 'redirects to google cloud deployments on enable service error' do
- get url
-
- expect(response).to redirect_to(project_google_cloud_deployments_path(project))
- # since GPC_PROJECT_ID is not set, enable cloud run service should return an error
- expect_snowplow_event(
- category: 'Projects::GoogleCloud::DeploymentsController',
- action: 'error_enable_services',
- label: nil,
- project: project,
- user: user_maintainer
- )
- end
+ context 'when enable service fails' do
+ before do
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
+ allow(service)
+ .to receive(:execute)
+ .and_return(
+ status: :error,
+ message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable'
+ )
+ end
+ end
- it 'redirects to google cloud deployments with error' do
- mock_gcp_error = Google::Apis::ClientError.new('some_error')
+ it 'redirects to google cloud deployments and tracks event on enable service error' do
+ get url
- allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
- allow(service).to receive(:execute).and_raise(mock_gcp_error)
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
+ # since GPC_PROJECT_ID is not set, enable cloud run service should return an error
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_enable_services',
+ label: nil,
+ project: project,
+ user: user_maintainer
+ )
end
- get url
+ it 'shows a flash alert' do
+ get url
- expect(response).to redirect_to(project_google_cloud_deployments_path(project))
- expect_snowplow_event(
- category: 'Projects::GoogleCloud::DeploymentsController',
- action: 'error_google_api',
- label: nil,
- project: project,
- user: user_maintainer
- )
+ expect(flash[:alert])
+ .to eq('No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable')
+ end
end
- context 'GCP_PROJECT_IDs are defined' do
- it 'redirects to google_cloud deployments on generate pipeline error' do
- allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
- allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
- end
+ context 'when enable service raises an error' do
+ before do
+ mock_gcp_error = Google::Apis::ClientError.new('some_error')
- allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service|
- allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error })
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
+ allow(service).to receive(:execute).and_raise(mock_gcp_error)
end
+ end
+ it 'redirects to google cloud deployments with error' do
get url
expect(response).to redirect_to(project_google_cloud_deployments_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud::DeploymentsController',
- action: 'error_generate_cloudrun_pipeline',
+ action: 'error_google_api',
label: nil,
project: project,
user: user_maintainer
)
end
- it 'redirects to create merge request form' do
- allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
- allow(service).to receive(:execute).and_return({ status: :success })
+ it 'shows a flash warning' do
+ get url
+
+ expect(flash[:warning]).to eq(format(_('Google Cloud Error - %{error}'), error: 'some_error'))
+ end
+ end
+
+ context 'GCP_PROJECT_IDs are defined' do
+ before do
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
+ allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
+ end
+ end
+
+ context 'when generate pipeline service fails' do
+ before do
+ allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service|
+ allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error })
+ end
+ end
+
+ it 'redirects to google_cloud deployments and tracks event on generate pipeline error' do
+ get url
+
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_generate_cloudrun_pipeline',
+ label: nil,
+ project: project,
+ user: user_maintainer
+ )
+ end
+
+ it 'shows a flash alert' do
+ get url
+
+ expect(flash[:alert]).to eq('Failed to generate pipeline')
end
+ end
+ it 'redirects to create merge request form' do
allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |service|
allow(service).to receive(:execute).and_return({ status: :success })
end
diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb
index 0535156b4b8..5678a81a4b0 100644
--- a/spec/requests/projects/issue_links_controller_spec.rb
+++ b/spec/requests/projects/issue_links_controller_spec.rb
@@ -28,22 +28,6 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning
context 'when linked issue is a task' do
let(:issue_b) { create :issue, :task, project: project }
- context 'when the use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'returns a work item path for the linked task' do
- get namespace_project_issue_links_path(issue_links_params)
-
- expect(json_response.count).to eq(1)
- expect(json_response.first).to include(
- 'path' => project_work_items_path(issue_b.project, issue_b.id),
- 'type' => 'TASK'
- )
- end
- end
-
it 'returns a work item path for the linked task using the iid in the path' do
get namespace_project_issue_links_path(issue_links_params)
diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb
index 67a73834f2d..29ea20210c7 100644
--- a/spec/requests/projects/issues_controller_spec.rb
+++ b/spec/requests/projects/issues_controller_spec.rb
@@ -25,33 +25,28 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
end
describe 'GET #show' do
- include_context 'group project issue'
+ before do
+ login_as(user)
+ end
it_behaves_like "observability csp policy", described_class do
+ include_context 'group project issue'
let(:tested_path) do
project_issue_path(project, issue)
end
end
- end
- describe 'GET #index.json' do
- let_it_be(:public_project) { create(:project, :public) }
+ describe 'incident tabs' do
+ let_it_be(:incident) { create(:incident, project: project) }
- it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do
- let_it_be(:current_user) { create(:user) }
-
- before do
- sign_in current_user
- end
-
- def request
- get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' }
+ it 'redirects to the issues route for non-incidents' do
+ get incident_issue_project_issue_path(project, issue, 'timeline')
+ expect(response).to redirect_to project_issue_path(project, issue)
end
- end
- it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated do
- def request
- get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' }
+ it 'responds with selected tab for incidents' do
+ get incident_issue_project_issue_path(project, incident, 'timeline')
+ expect(response.body).to match(/&quot;currentTab&quot;:&quot;timeline&quot;/)
end
end
end
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index d82fa284a42..54ee5e489f7 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
.to change { Gitlab::GitalyClient.get_request_count }.by_at_most(4)
end
- context 'caching', :use_clean_rails_memory_store_caching do
+ context 'caching' do
let(:reference) { create(:issue, project: project) }
let(:author) { create(:user) }
let!(:first_note) { create(:diff_note_on_merge_request, author: author, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") }
@@ -81,193 +81,180 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
shared_examples 'cache hit' do
it 'gets cached on subsequent requests' do
- expect_next_instance_of(DiscussionSerializer) do |serializer|
- expect(serializer).not_to receive(:represent)
- end
+ expect(DiscussionSerializer).not_to receive(:new)
send_request
end
end
- context 'when mr_discussions_http_cache and disabled_mr_discussions_redis_cache are enabled' do
- before do
- send_request
- end
-
- it_behaves_like 'cache hit'
+ before do
+ send_request
+ end
- context 'when a note in a discussion got updated' do
- before do
- first_note.update!(updated_at: 1.minute.from_now)
- end
+ it_behaves_like 'cache hit'
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ context 'when a note in a discussion got updated' do
+ before do
+ first_note.update!(updated_at: 1.minute.from_now)
end
- context 'when a note in a discussion got its reference state updated' do
- before do
- reference.close!
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ context 'when a note in a discussion got its reference state updated' do
+ before do
+ reference.close!
end
- context 'when a note in a discussion got resolved' do
- before do
- travel_to(1.minute.from_now) do
- first_note.resolve!(user)
- end
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
+ context 'when a note in a discussion got resolved' do
+ before do
+ travel_to(1.minute.from_now) do
+ first_note.resolve!(user)
end
end
- context 'when a note is added to a discussion' do
- let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
-
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note, third_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when a note is removed from a discussion' do
- before do
- second_note.destroy!
- end
+ context 'when a note is added to a discussion' do
+ let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note, third_note] }
end
+ end
- context 'when an emoji is awarded to a note in discussion' do
- before do
- travel_to(1.minute.from_now) do
- create(:award_emoji, awardable: first_note)
- end
- end
+ context 'when a note is removed from a discussion' do
+ before do
+ second_note.destroy!
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note] }
end
+ end
- context 'when an award emoji is removed from a note in discussion' do
- before do
- travel_to(1.minute.from_now) do
- award_emoji.destroy!
- end
+ context 'when an emoji is awarded to a note in discussion' do
+ before do
+ travel_to(1.minute.from_now) do
+ create(:award_emoji, awardable: first_note)
end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when the diff note position changes' do
- before do
- # This replicates a position change wherein timestamps aren't updated
- # which is why `Gitlab::Timeless.timeless` is utilized. This is the
- # same approach being used in Discussions::UpdateDiffPositionService
- # which is responsible for updating the positions of diff discussions
- # when MR updates.
- first_note.position = Gitlab::Diff::Position.new(
- old_path: first_note.position.old_path,
- new_path: first_note.position.new_path,
- old_line: first_note.position.old_line,
- new_line: first_note.position.new_line + 1,
- diff_refs: first_note.position.diff_refs
- )
-
- Gitlab::Timeless.timeless(first_note, &:save)
+ context 'when an award emoji is removed from a note in discussion' do
+ before do
+ travel_to(1.minute.from_now) do
+ award_emoji.destroy!
end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when the HEAD diff note position changes' do
- before do
- # This replicates a DiffNotePosition change. This is the same approach
- # being used in Discussions::CaptureDiffNotePositionService which is
- # responsible for updating/creating DiffNotePosition of a diff discussions
- # in relation to HEAD diff.
- new_position = Gitlab::Diff::Position.new(
- old_path: first_note.position.old_path,
- new_path: first_note.position.new_path,
- old_line: first_note.position.old_line,
- new_line: first_note.position.new_line + 1,
- diff_refs: first_note.position.diff_refs
- )
-
- DiffNotePosition.create_or_update_for(
- first_note,
- diff_type: :head,
- position: new_position,
- line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
- )
- end
+ context 'when the diff note position changes' do
+ before do
+ # This replicates a position change wherein timestamps aren't updated
+ # which is why `Gitlab::Timeless.timeless` is utilized. This is the
+ # same approach being used in Discussions::UpdateDiffPositionService
+ # which is responsible for updating the positions of diff discussions
+ # when MR updates.
+ first_note.position = Gitlab::Diff::Position.new(
+ old_path: first_note.position.old_path,
+ new_path: first_note.position.new_path,
+ old_line: first_note.position.old_line,
+ new_line: first_note.position.new_line + 1,
+ diff_refs: first_note.position.diff_refs
+ )
+
+ Gitlab::Timeless.timeless(first_note, &:save)
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author detail changes' do
- before do
- author.update!(name: "#{author.name} (Updated)")
- end
+ context 'when the HEAD diff note position changes' do
+ before do
+ # This replicates a DiffNotePosition change. This is the same approach
+ # being used in Discussions::CaptureDiffNotePositionService which is
+ # responsible for updating/creating DiffNotePosition of a diff discussions
+ # in relation to HEAD diff.
+ new_position = Gitlab::Diff::Position.new(
+ old_path: first_note.position.old_path,
+ new_path: first_note.position.new_path,
+ old_line: first_note.position.old_line,
+ new_line: first_note.position.new_line + 1,
+ diff_refs: first_note.position.diff_refs
+ )
+
+ DiffNotePosition.create_or_update_for(
+ first_note,
+ diff_type: :head,
+ position: new_position,
+ line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
+ )
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author status changes' do
- before do
- Users::SetStatusService.new(author, message: "updated status").execute
- end
+ context 'when author detail changes' do
+ before do
+ author.update!(name: "#{author.name} (Updated)")
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author role changes' do
- before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership)
- end
+ context 'when author status changes' do
+ before do
+ Users::SetStatusService.new(author, message: "updated status").execute
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when current_user role changes' do
- before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
- end
+ context 'when author role changes' do
+ before do
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership)
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
end
- context 'when disabled_mr_discussions_redis_cache is disabled' do
+ context 'when current_user role changes' do
before do
- stub_feature_flags(disabled_mr_discussions_redis_cache: false)
- send_request
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
end
- it_behaves_like 'cache hit'
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
end
end
end
diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
index defb35fd496..666dc42bcab 100644
--- a/spec/requests/projects/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::AccessTokensController, feature_category: :authentication_and_authorization do
+RSpec.describe Projects::Settings::AccessTokensController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:resource) { create(:project, group: group) }
diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb
index aec2636b69c..a591f479763 100644
--- a/spec/requests/projects/uploads_spec.rb
+++ b/spec/requests/projects/uploads_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'File uploads', feature_category: :not_owned do
+RSpec.describe 'File uploads', feature_category: :shared do
include WorkhorseHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/projects/wikis_controller_spec.rb b/spec/requests/projects/wikis_controller_spec.rb
new file mode 100644
index 00000000000..4768e7134e8
--- /dev/null
+++ b/spec/requests/projects/wikis_controller_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::WikisController, feature_category: :wiki do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) }
+ let_it_be(:wiki_page) do
+ create(:wiki_page,
+ wiki: project_wiki,
+ title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})")
+ end
+
+ let_it_be(:csp_nonce) { 'just=some=noncense' }
+
+ before do
+ sign_in(user)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce)
+ end
+ end
+
+ shared_examples 'embed.diagrams.net frame-src directive' do
+ it 'adds drawio frame-src directive to the Content Security Policy header' do
+ frame_src = response.headers['Content-Security-Policy'].split(';')
+ .map(&:strip)
+ .find { |entry| entry.starts_with?('frame-src') }
+
+ expect(frame_src).to include('https://embed.diagrams.net')
+ end
+ end
+
+ describe 'CSP policy' do
+ describe '#new' do
+ before do
+ get wiki_path(project_wiki, action: :new)
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+
+ describe '#edit' do
+ before do
+ get wiki_page_path(project_wiki, wiki_page, action: 'edit')
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+
+ describe '#create' do
+ before do
+ # Creating a page with an invalid title to render edit page
+ post wiki_path(project_wiki, action: 'create'), params: { wiki: { title: 'home' } }
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+
+ describe '#update' do
+ before do
+ # Setting an invalid page title to render edit page
+ put wiki_page_path(project_wiki, wiki_page), params: { wiki: { title: '' } }
+ print(response.body)
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+ end
+end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 91595f7826a..0dd8a15c3a4 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
include RackAttackSpecHelpers
include SessionHelpers
diff --git a/spec/requests/sandbox_controller_spec.rb b/spec/requests/sandbox_controller_spec.rb
index 77913065380..26a7422680c 100644
--- a/spec/requests/sandbox_controller_spec.rb
+++ b/spec/requests/sandbox_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SandboxController, feature_category: :not_owned do
+RSpec.describe SandboxController, feature_category: :shared do
describe 'GET #mermaid' do
it 'renders page without template' do
get sandbox_mermaid_path
diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb
index 7b3fd23980a..bc4ac3b7335 100644
--- a/spec/requests/sessions_spec.rb
+++ b/spec/requests/sessions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Sessions', feature_category: :authentication_and_authorization do
+RSpec.describe 'Sessions', feature_category: :system_access do
context 'authentication', :allow_forgery_protection do
let(:user) { create(:user) }
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 11d8be24e06..75ae0c970c0 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -174,39 +174,95 @@ RSpec.describe UsersController, feature_category: :user_management do
end
context 'requested in json format' do
- let(:project) { create(:project) }
+ context 'when profile_tabs_vue feature flag is turned OFF' do
+ let(:project) { create(:project) }
- before do
- project.add_developer(user)
- Gitlab::DataBuilder::Push.build_sample(project, user)
+ before do
+ project.add_developer(user)
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ stub_feature_flags(profile_tabs_vue: false)
+ sign_in(user)
+ end
- sign_in(user)
- end
+ it 'loads events' do
+ get user_activity_url user.username, format: :json
- it 'loads events' do
- get user_activity_url user.username, format: :json
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body)['count']).to eq(1)
+ end
- expect(response.media_type).to eq('application/json')
- expect(Gitlab::Json.parse(response.body)['count']).to eq(1)
- end
+ it 'hides events if the user cannot read cross project' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
- it 'hides events if the user cannot read cross project' do
- allow(Ability).to receive(:allowed?).and_call_original
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+ get user_activity_url user.username, format: :json
- get user_activity_url user.username, format: :json
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ end
- expect(response.media_type).to eq('application/json')
- expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ it 'hides events if the user has a private profile' do
+ Gitlab::DataBuilder::Push.build_sample(project, private_user)
+
+ get user_activity_url private_user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ end
end
- it 'hides events if the user has a private profile' do
- Gitlab::DataBuilder::Push.build_sample(project, private_user)
+ context 'when profile_tabs_vue feature flag is turned ON' do
+ let(:project) { create(:project) }
- get user_activity_url private_user.username, format: :json
+ before do
+ project.add_developer(user)
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ stub_feature_flags(profile_tabs_vue: true)
+ sign_in(user)
+ end
- expect(response.media_type).to eq('application/json')
- expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ it 'loads events' do
+ get user_activity_url user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body).count).to eq(1)
+ end
+
+ it 'hides events if the user cannot read cross project' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+
+ get user_activity_url user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body).count).to eq(0)
+ end
+
+ it 'hides events if the user has a private profile' do
+ Gitlab::DataBuilder::Push.build_sample(project, private_user)
+
+ get user_activity_url private_user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body).count).to eq(0)
+ end
+
+ it 'hides events if the user has a private profile' do
+ project = create(:project, :private)
+ private_event_user = create(:user, include_private_contributions: true)
+ push_data = Gitlab::DataBuilder::Push.build_sample(project, private_event_user)
+ EventCreateService.new.push(project, private_event_user, push_data)
+
+ get user_activity_url private_event_user.username, format: :json
+
+ response_body = Gitlab::Json.parse(response.body)
+ event = response_body.first
+ expect(response.media_type).to eq('application/json')
+ expect(response_body.count).to eq(1)
+ expect(event).to include('created_at', 'author', 'action')
+ expect(event['action']).to eq('private')
+ expect(event).not_to include('ref', 'commit', 'target', 'resource_parent')
+ end
end
end
end
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index 8a6a7e717ff..3184fe151be 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -42,7 +42,7 @@ feature_category: :user_management do
shared_examples_for 'two factor prompt or successful login' do
it 'shows the 2FA prompt when enabled or redirects to the root path' do
if user.two_factor_enabled?
- expect(response.body).to include('Two-factor authentication code')
+ expect(response.body).to include('Enter verification code')
else
expect(response).to redirect_to(root_path)
end
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index ac3f2a4b7ca..4d6090d6027 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -75,6 +75,10 @@ RSpec.describe Import::GithubController, 'routing' do
it 'to #cancel_all' do
expect(post('/import/github/cancel_all')).to route_to('import/github#cancel_all')
end
+
+ it 'to #counts' do
+ expect(get('/import/github/counts')).to route_to('import/github#counts')
+ end
end
# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token
diff --git a/spec/routing/user_routing_spec.rb b/spec/routing/user_routing_spec.rb
index 7bb589565fa..b155560c9f0 100644
--- a/spec/routing/user_routing_spec.rb
+++ b/spec/routing/user_routing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'user routing', :clean_gitlab_redis_sessions, feature_category: :authentication_and_authorization do
+RSpec.describe 'user routing', :clean_gitlab_redis_sessions, feature_category: :system_access do
include SessionHelpers
context 'when GitHub OAuth on project import is cancelled' do
diff --git a/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb b/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb
new file mode 100644
index 00000000000..32b958426b9
--- /dev/null
+++ b/spec/rubocop/cop/background_migration/missing_dictionary_file_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/background_migration/missing_dictionary_file'
+
+RSpec.describe RuboCop::Cop::BackgroundMigration::MissingDictionaryFile, feature_category: :database do
+ let(:config) do
+ RuboCop::Config.new(
+ 'BackgroundMigration/MissingDictionaryFile' => {
+ 'EnforcedSince' => 20230307160251
+ }
+ )
+ end
+
+ context 'for non post migrations' do
+ before do
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
+ end
+
+ it 'does not throw any offense' do
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'for post migrations' do
+ before do
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(true)
+ end
+
+ context 'without enqueuing batched migrations' do
+ it 'does not throw any offense' do
+ expect_no_offenses(<<~RUBY)
+ class CreateTestTable < Gitlab::Database::Migration[2.1]
+ def change
+ create_table(:tests)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'with enqueuing batched migration' do
+ let(:rails_root) { File.expand_path('../../../..', __dir__) }
+ let(:dictionary_file_path) { File.join(rails_root, 'db/docs/batched_background_migrations/my_migration.yml') }
+
+ context 'for migrations before enforced time' do
+ before do
+ allow(cop).to receive(:version).and_return(20230307160250)
+ end
+
+ it 'does not throw any offenses' do
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'for migrations after enforced time' do
+ before do
+ allow(cop).to receive(:version).and_return(20230307160252)
+ end
+
+ it 'throws offense on not having the appropriate dictionary file with migration name as a constant' do
+ expect_offense(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)}
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+
+ it 'throws offense on not having the appropriate dictionary file with migration name as a variable' do
+ expect_offense(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{format("Missing %{file_name}. Use the generator 'batched_background_migration' to create dictionary files automatically. For more details refer: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#generator", file_name: dictionary_file_path)}
+ def up
+ queue_batched_background_migration(
+ 'MyMigration',
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+
+ it 'does not throw offense with appropriate dictionary file' do
+ expect(File).to receive(:exist?).with(dictionary_file_path).and_return(true)
+
+ expect_no_offenses(<<~RUBY)
+ class QueueMyMigration < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MyMigration'
+
+ def up
+ queue_batched_background_migration(
+ MIGRATION,
+ :users,
+ :id
+ )
+ end
+ end
+ RUBY
+ end
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/doc_url_spec.rb b/spec/rubocop/cop/gitlab/doc_url_spec.rb
index 4a7ef14ccbc..957edc8286b 100644
--- a/spec/rubocop/cop/gitlab/doc_url_spec.rb
+++ b/spec/rubocop/cop/gitlab/doc_url_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/gitlab/doc_url'
-RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :not_owned do
+RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :shared do
context 'when string literal is added with docs url prefix' do
context 'when inlined' do
it 'registers an offense' do
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 bfc0cebe203..9d550d9c56e 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -205,14 +205,4 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
include_examples 'sets flag as used', 'deduplicate :delayed, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'deduplicate :delayed'
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'] }
-
- before do
- allow(cop).to receive(:usage_data_counters_known_event_feature_flags).and_return(usage_data_counters_known_event_feature_flags)
- end
-
- include_examples 'sets flag as used', "FEATURE_FLAG = :foo", %w[foo an_event_feature_flag]
- end
end
diff --git a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
index 53f19cd01ee..edd54a40b79 100644
--- a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
+++ b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/lint/last_keyword_argument'
-RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument, :ruby27, feature_category: :not_owned do
+RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument, :ruby27, feature_category: :shared do
before do
described_class.instance_variable_set(:@keyword_warnings, nil)
allow(Dir).to receive(:glob).and_call_original
diff --git a/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb b/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb
index b180134b226..db8c7b1d783 100644
--- a/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb
+++ b/spec/rubocop/cop/rspec/avoid_test_prof_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require_relative '../../../../rubocop/cop/rspec/avoid_test_prof'
-RSpec.describe RuboCop::Cop::RSpec::AvoidTestProf, feature_category: :not_owned do
+RSpec.describe RuboCop::Cop::RSpec::AvoidTestProf, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
context 'when there are offenses' do
diff --git a/spec/scripts/database/schema_validator_spec.rb b/spec/scripts/database/schema_validator_spec.rb
new file mode 100644
index 00000000000..13be8e291da
--- /dev/null
+++ b/spec/scripts/database/schema_validator_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../scripts/database/schema_validator'
+
+RSpec.describe SchemaValidator, feature_category: :database do
+ subject(:validator) { described_class.new }
+
+ describe "#validate!" do
+ before do
+ allow(validator).to receive(:committed_migrations).and_return(committed_migrations)
+ allow(validator).to receive(:run).and_return(schema_changes)
+ end
+
+ context 'when schema changes are introduced without migrations' do
+ let(:committed_migrations) { [] }
+ let(:schema_changes) { 'db/structure.sql' }
+
+ it 'terminates the execution' do
+ expect { validator.validate! }.to raise_error(SystemExit)
+ end
+ end
+
+ context 'when schema changes are introduced with migrations' do
+ let(:committed_migrations) { ['20211006103122_my_migration.rb'] }
+ let(:schema_changes) { 'db/structure.sql' }
+ let(:command) { 'git diff db/structure.sql -- db/structure.sql' }
+ let(:base_message) { 'db/structure.sql was changed, and no migrations were added' }
+
+ before do
+ allow(validator).to receive(:die)
+ end
+
+ it 'skips schema validations' do
+ expect(validator.validate!).to be_nil
+ end
+ end
+
+ context 'when skipping validations through ENV variable' do
+ let(:committed_migrations) { [] }
+ let(:schema_changes) { 'db/structure.sql' }
+
+ before do
+ stub_env('ALLOW_SCHEMA_CHANGES', true)
+ end
+
+ it 'skips schema validations' do
+ expect(validator.validate!).to be_nil
+ end
+ end
+
+ context 'when skipping validations through commit message' do
+ let(:committed_migrations) { [] }
+ let(:schema_changes) { 'db/structure.sql' }
+ let(:commit_message) { "Changes db/strucure.sql file\nskip-db-structure-check" }
+
+ before do
+ allow(validator).to receive(:run).and_return(commit_message)
+ end
+
+ it 'skips schema validations' do
+ expect(validator.validate!).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/scripts/generate_rspec_pipeline_spec.rb b/spec/scripts/generate_rspec_pipeline_spec.rb
new file mode 100644
index 00000000000..b3eaf9e9127
--- /dev/null
+++ b/spec/scripts/generate_rspec_pipeline_spec.rb
@@ -0,0 +1,198 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'tempfile'
+
+require_relative '../../scripts/generate_rspec_pipeline'
+
+RSpec.describe GenerateRspecPipeline, :silence_stdout, feature_category: :tooling do
+ describe '#generate!' do
+ let!(:rspec_files) { Tempfile.new(['rspec_files_path', '.txt']) }
+ let(:rspec_files_content) do
+ "spec/migrations/a_spec.rb spec/migrations/b_spec.rb " \
+ "spec/lib/gitlab/background_migration/a_spec.rb spec/lib/gitlab/background_migration/b_spec.rb " \
+ "spec/models/a_spec.rb spec/models/b_spec.rb " \
+ "spec/controllers/a_spec.rb spec/controllers/b_spec.rb " \
+ "spec/features/a_spec.rb spec/features/b_spec.rb"
+ end
+
+ let(:pipeline_template) { Tempfile.new(['pipeline_template', '.yml.erb']) }
+ let(:pipeline_template_content) do
+ <<~YAML
+ <% if rspec_files_per_test_level[:migration][:files].size > 0 %>
+ rspec migration:
+ <% if rspec_files_per_test_level[:migration][:parallelization] > 1 %>
+ parallel: <%= rspec_files_per_test_level[:migration][:parallelization] %>
+ <% end %>
+ <% end %>
+ <% if rspec_files_per_test_level[:background_migration][:files].size > 0 %>
+ rspec background_migration:
+ <% if rspec_files_per_test_level[:background_migration][:parallelization] > 1 %>
+ parallel: <%= rspec_files_per_test_level[:background_migration][:parallelization] %>
+ <% end %>
+ <% end %>
+ <% if rspec_files_per_test_level[:unit][:files].size > 0 %>
+ rspec unit:
+ <% if rspec_files_per_test_level[:unit][:parallelization] > 1 %>
+ parallel: <%= rspec_files_per_test_level[:unit][:parallelization] %>
+ <% end %>
+ <% end %>
+ <% if rspec_files_per_test_level[:integration][:files].size > 0 %>
+ rspec integration:
+ <% if rspec_files_per_test_level[:integration][:parallelization] > 1 %>
+ parallel: <%= rspec_files_per_test_level[:integration][:parallelization] %>
+ <% end %>
+ <% end %>
+ <% if rspec_files_per_test_level[:system][:files].size > 0 %>
+ rspec system:
+ <% if rspec_files_per_test_level[:system][:parallelization] > 1 %>
+ parallel: <%= rspec_files_per_test_level[:system][:parallelization] %>
+ <% end %>
+ <% end %>
+ YAML
+ end
+
+ let(:knapsack_report) { Tempfile.new(['knapsack_report', '.json']) }
+ let(:knapsack_report_content) do
+ <<~JSON
+ {
+ "spec/migrations/a_spec.rb": 360.3,
+ "spec/migrations/b_spec.rb": 180.1,
+ "spec/lib/gitlab/background_migration/a_spec.rb": 60.5,
+ "spec/lib/gitlab/background_migration/b_spec.rb": 180.3,
+ "spec/models/a_spec.rb": 360.2,
+ "spec/models/b_spec.rb": 180.6,
+ "spec/controllers/a_spec.rb": 60.2,
+ "spec/controllers/ab_spec.rb": 180.4,
+ "spec/features/a_spec.rb": 360.1,
+ "spec/features/b_spec.rb": 180.5
+ }
+ JSON
+ end
+
+ around do |example|
+ rspec_files.write(rspec_files_content)
+ rspec_files.rewind
+ pipeline_template.write(pipeline_template_content)
+ pipeline_template.rewind
+ knapsack_report.write(knapsack_report_content)
+ knapsack_report.rewind
+ example.run
+ ensure
+ rspec_files.close
+ rspec_files.unlink
+ pipeline_template.close
+ pipeline_template.unlink
+ knapsack_report.close
+ knapsack_report.unlink
+ end
+
+ context 'when rspec_files and pipeline_template_path exists' do
+ subject do
+ described_class.new(
+ rspec_files_path: rspec_files.path,
+ pipeline_template_path: pipeline_template.path
+ )
+ end
+
+ it 'generates the pipeline config with default parallelization' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml"))
+ .to eq(
+ "rspec migration:\nrspec background_migration:\nrspec unit:\n" \
+ "rspec integration:\nrspec system:"
+ )
+ end
+
+ context 'when parallelization > 0' do
+ before do
+ stub_const("#{described_class}::DEFAULT_AVERAGE_TEST_FILE_DURATION_IN_SECONDS", 360)
+ end
+
+ it 'generates the pipeline config' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml"))
+ .to eq(
+ "rspec migration:\n parallel: 2\nrspec background_migration:\n parallel: 2\n" \
+ "rspec unit:\n parallel: 2\nrspec integration:\n parallel: 2\n" \
+ "rspec system:\n parallel: 2"
+ )
+ end
+ end
+
+ context 'when parallelization > MAX_NODES_COUNT' do
+ let(:rspec_files_content) do
+ Array.new(51) { |i| "spec/migrations/#{i}_spec.rb" }.join(' ')
+ end
+
+ before do
+ stub_const(
+ "#{described_class}::DEFAULT_AVERAGE_TEST_FILE_DURATION_IN_SECONDS",
+ described_class::OPTIMAL_TEST_JOB_DURATION_IN_SECONDS
+ )
+ end
+
+ it 'generates the pipeline config with max parallelization of 50' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml")).to eq("rspec migration:\n parallel: 50")
+ end
+ end
+ end
+
+ context 'when knapsack_report_path is given' do
+ subject do
+ described_class.new(
+ rspec_files_path: rspec_files.path,
+ pipeline_template_path: pipeline_template.path,
+ knapsack_report_path: knapsack_report.path
+ )
+ end
+
+ it 'generates the pipeline config with parallelization based on Knapsack' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml"))
+ .to eq(
+ "rspec migration:\n parallel: 2\nrspec background_migration:\n" \
+ "rspec unit:\n parallel: 2\nrspec integration:\n" \
+ "rspec system:\n parallel: 2"
+ )
+ end
+
+ context 'and Knapsack report does not contain valid JSON' do
+ let(:knapsack_report_content) { "#{super()}," }
+
+ it 'generates the pipeline config with default parallelization' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml"))
+ .to eq(
+ "rspec migration:\nrspec background_migration:\nrspec unit:\n" \
+ "rspec integration:\nrspec system:"
+ )
+ end
+ end
+ end
+
+ context 'when rspec_files does not exist' do
+ subject { described_class.new(rspec_files_path: nil, pipeline_template_path: pipeline_template.path) }
+
+ it 'generates the pipeline config using the no-op template' do
+ subject.generate!
+
+ expect(File.read("#{pipeline_template.path}.yml")).to include("no-op:")
+ end
+ end
+
+ context 'when pipeline_template_path does not exist' do
+ subject { described_class.new(rspec_files_path: rspec_files.path, pipeline_template_path: nil) }
+
+ it 'generates the pipeline config using the no-op template' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/scripts/lib/glfm/shared_spec.rb b/spec/scripts/lib/glfm/shared_spec.rb
index 3717c7ce18f..d407bd49d75 100644
--- a/spec/scripts/lib/glfm/shared_spec.rb
+++ b/spec/scripts/lib/glfm/shared_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'tmpdir'
require_relative '../../../../scripts/lib/glfm/shared'
-RSpec.describe Glfm::Shared do
+RSpec.describe Glfm::Shared, feature_category: :team_planning do
let(:instance) do
Class.new do
include Glfm::Shared
diff --git a/spec/scripts/pipeline/create_test_failure_issues_spec.rb b/spec/scripts/pipeline/create_test_failure_issues_spec.rb
new file mode 100644
index 00000000000..fa27727542e
--- /dev/null
+++ b/spec/scripts/pipeline/create_test_failure_issues_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+# rubocop:disable RSpec/VerifiedDoubles
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../scripts/pipeline/create_test_failure_issues'
+
+RSpec.describe CreateTestFailureIssues, feature_category: :tooling do
+ describe CreateTestFailureIssue do
+ let(:env) do
+ {
+ 'CI_JOB_URL' => 'ci_job_url',
+ 'CI_PIPELINE_URL' => 'ci_pipeline_url'
+ }
+ end
+
+ let(:project) { 'group/project' }
+ let(:api_token) { 'api_token' }
+ let(:creator) { described_class.new(project: project, api_token: api_token) }
+ let(:test_name) { 'The test description' }
+ let(:test_file) { 'spec/path/to/file_spec.rb' }
+ let(:test_file_content) do
+ <<~CONTENT
+ # comment
+
+ RSpec.describe Foo, feature_category: :source_code_management do
+ end
+
+ CONTENT
+ end
+
+ let(:test_file_stub) { double(read: test_file_content) }
+ let(:failed_test) do
+ {
+ 'name' => test_name,
+ 'file' => test_file,
+ 'job_url' => 'job_url'
+ }
+ end
+
+ let(:categories_mapping) do
+ {
+ 'source_code_management' => {
+ 'group' => 'source_code',
+ 'label' => 'Category:Source Code Management'
+ }
+ }
+ end
+
+ let(:groups_mapping) do
+ {
+ 'source_code' => {
+ 'label' => 'group::source_code'
+ }
+ }
+ end
+
+ before do
+ stub_env(env)
+ end
+
+ describe '#find' do
+ let(:expected_payload) do
+ {
+ state: 'opened',
+ search: "#{failed_test['file']} - ID: #{Digest::SHA256.hexdigest(failed_test['name'])[0...12]}"
+ }
+ end
+
+ let(:find_issue_stub) { double('FindIssues') }
+ let(:issue_stub) { double(title: expected_payload[:title], web_url: 'issue_web_url') }
+
+ before do
+ allow(creator).to receive(:puts)
+ end
+
+ it 'calls FindIssues#execute(payload)' do
+ expect(FindIssues).to receive(:new).with(project: project, api_token: api_token).and_return(find_issue_stub)
+ expect(find_issue_stub).to receive(:execute).with(expected_payload).and_return([issue_stub])
+
+ creator.find(failed_test)
+ end
+
+ context 'when no issues are found' do
+ it 'calls FindIssues#execute(payload)' do
+ expect(FindIssues).to receive(:new).with(project: project, api_token: api_token).and_return(find_issue_stub)
+ expect(find_issue_stub).to receive(:execute).with(expected_payload).and_return([])
+
+ creator.find(failed_test)
+ end
+ end
+ end
+
+ describe '#create' do
+ let(:expected_description) do
+ <<~DESCRIPTION
+ ### Full description
+
+ `#{failed_test['name']}`
+
+ ### File path
+
+ `#{failed_test['file']}`
+
+ <!-- Don't add anything after the report list since it's updated automatically -->
+ ### Reports
+
+ - #{failed_test['job_url']} (#{env['CI_PIPELINE_URL']})
+ DESCRIPTION
+ end
+
+ let(:expected_payload) do
+ {
+ title: "#{failed_test['file']} - ID: #{Digest::SHA256.hexdigest(failed_test['name'])[0...12]}",
+ description: expected_description,
+ labels: described_class::DEFAULT_LABELS.map { |label| "wip-#{label}" } + [
+ "wip-#{categories_mapping['source_code_management']['label']}", "wip-#{groups_mapping['source_code']['label']}" # rubocop:disable Layout/LineLength
+ ]
+ }
+ end
+
+ let(:create_issue_stub) { double('CreateIssue') }
+ let(:issue_stub) { double(title: expected_payload[:title], web_url: 'issue_web_url') }
+
+ before do
+ allow(creator).to receive(:puts)
+ allow(File).to receive(:open).and_call_original
+ allow(File).to receive(:open).with(File.expand_path(File.join('..', '..', '..', test_file), __dir__))
+ .and_return(test_file_stub)
+ allow(creator).to receive(:categories_mapping).and_return(categories_mapping)
+ allow(creator).to receive(:groups_mapping).and_return(groups_mapping)
+ end
+
+ it 'calls CreateIssue#execute(payload)' do
+ expect(CreateIssue).to receive(:new).with(project: project, api_token: api_token).and_return(create_issue_stub)
+ expect(create_issue_stub).to receive(:execute).with(expected_payload).and_return(issue_stub)
+
+ creator.create(failed_test) # rubocop:disable Rails/SaveBang
+ end
+ end
+ end
+end
+# rubocop:enable RSpec/VerifiedDoubles
diff --git a/spec/scripts/pipeline_test_report_builder_spec.rb b/spec/scripts/pipeline_test_report_builder_spec.rb
index e7529eb0d41..bee2a4a5835 100644
--- a/spec/scripts/pipeline_test_report_builder_spec.rb
+++ b/spec/scripts/pipeline_test_report_builder_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe PipelineTestReportBuilder, feature_category: :tooling do
let(:options) do
described_class::DEFAULT_OPTIONS.merge(
target_project: 'gitlab-org/gitlab',
+ current_pipeline_id: '42',
mr_id: '999',
instance_base_url: 'https://gitlab.com',
output_file_path: output_file_path
@@ -191,10 +192,14 @@ RSpec.describe PipelineTestReportBuilder, feature_category: :tooling do
context 'for latest pipeline' do
let(:failed_build_uri) { "#{latest_pipeline_url}/tests/suite.json?build_ids[]=#{failed_build_id}" }
+ let(:current_pipeline_uri) do
+ "#{options[:api_endpoint]}/projects/#{options[:target_project]}/pipelines/#{options[:current_pipeline_id]}"
+ end
subject { described_class.new(options.merge(pipeline_index: :latest)) }
it 'fetches builds from pipeline related to MR' do
+ expect(subject).to receive(:fetch).with(current_pipeline_uri).and_return(mr_pipelines[0])
expect(subject).to receive(:fetch).with(failed_build_uri).and_return(test_report_for_build)
subject.test_report_for_pipeline
diff --git a/spec/scripts/review_apps/automated_cleanup_spec.rb b/spec/scripts/review_apps/automated_cleanup_spec.rb
new file mode 100644
index 00000000000..546bf55a934
--- /dev/null
+++ b/spec/scripts/review_apps/automated_cleanup_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'time'
+require_relative '../../../scripts/review_apps/automated_cleanup'
+
+RSpec.describe ReviewApps::AutomatedCleanup, feature_category: :tooling do
+ let(:instance) { described_class.new(options: options) }
+ let(:options) do
+ {
+ project_path: 'my-project-path',
+ gitlab_token: 'glpat-test-secret-token',
+ api_endpoint: 'gitlab.test/api/v4',
+ dry_run: dry_run
+ }
+ end
+
+ let(:kubernetes_client) { instance_double(Tooling::KubernetesClient) }
+ let(:helm_client) { instance_double(Tooling::Helm3Client) }
+ let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles
+ let(:dry_run) { false }
+ let(:now) { Time.now }
+ let(:one_day_ago) { (now - (1 * 24 * 3600)) }
+ let(:two_days_ago) { (now - (2 * 24 * 3600)) }
+ let(:three_days_ago) { (now - (3 * 24 * 3600)) }
+
+ before do
+ allow(instance).to receive(:gitlab).and_return(gitlab_client)
+ allow(Time).to receive(:now).and_return(now)
+ allow(Tooling::Helm3Client).to receive(:new).and_return(helm_client)
+ allow(Tooling::KubernetesClient).to receive(:new).and_return(kubernetes_client)
+
+ allow(kubernetes_client).to receive(:cleanup_by_created_at)
+ allow(kubernetes_client).to receive(:cleanup_by_release)
+ allow(kubernetes_client).to receive(:cleanup_review_app_namespaces)
+ allow(kubernetes_client).to receive(:delete_namespaces_by_exact_names)
+ end
+
+ shared_examples 'the days argument is an integer in the correct range' do
+ context 'when days is nil' do
+ let(:days) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('days should be an integer between 1 and 365 inclusive! Got 0')
+ end
+ end
+
+ context 'when days is zero' do
+ let(:days) { 0 }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('days should be an integer between 1 and 365 inclusive! Got 0')
+ end
+ end
+
+ context 'when days is above 365' do
+ let(:days) { 366 }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('days should be an integer between 1 and 365 inclusive! Got 366')
+ end
+ end
+
+ context 'when days is a string' do
+ let(:days) { '10' }
+
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when days is a float' do
+ let(:days) { 3.0 }
+
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#perform_stale_pvc_cleanup!' do
+ subject { instance.perform_stale_pvc_cleanup!(days: days) }
+
+ let(:days) { 2 }
+
+ it_behaves_like 'the days argument is an integer in the correct range'
+
+ it 'performs Kubernetes cleanup by created at' do
+ expect(kubernetes_client).to receive(:cleanup_by_created_at).with(
+ resource_type: 'pvc',
+ created_before: two_days_ago,
+ wait: false
+ )
+
+ subject
+ end
+
+ context 'when the dry-run flag is true' do
+ let(:dry_run) { true }
+
+ it 'does not delete anything' do
+ expect(kubernetes_client).not_to receive(:cleanup_by_created_at)
+ end
+ end
+ end
+
+ describe '#perform_stale_namespace_cleanup!' do
+ subject { instance.perform_stale_namespace_cleanup!(days: days) }
+
+ let(:days) { 2 }
+
+ it_behaves_like 'the days argument is an integer in the correct range'
+
+ it 'performs Kubernetes cleanup for review apps namespaces' do
+ expect(kubernetes_client).to receive(:cleanup_review_app_namespaces).with(
+ created_before: two_days_ago,
+ wait: false
+ )
+
+ subject
+ end
+
+ context 'when the dry-run flag is true' do
+ let(:dry_run) { true }
+
+ it 'does not delete anything' do
+ expect(kubernetes_client).not_to receive(:cleanup_review_app_namespaces)
+ end
+ end
+ end
+
+ describe '#perform_helm_releases_cleanup!' do
+ subject { instance.perform_helm_releases_cleanup!(days: days) }
+
+ let(:days) { 2 }
+ let(:helm_releases) { [] }
+
+ before do
+ allow(helm_client).to receive(:releases).and_return(helm_releases)
+
+ # Silence outputs to stdout
+ allow(instance).to receive(:puts)
+ end
+
+ shared_examples 'deletes the helm release' do
+ let(:releases_names) { helm_releases.map(&:name) }
+
+ before do
+ allow(helm_client).to receive(:delete)
+ allow(kubernetes_client).to receive(:cleanup_by_release)
+ allow(kubernetes_client).to receive(:delete_namespaces_by_exact_names)
+ end
+
+ it 'deletes the helm release' do
+ expect(helm_client).to receive(:delete).with(release_name: releases_names)
+
+ subject
+ end
+
+ it 'empties the k8s resources in the k8s namespace for the release' do
+ expect(kubernetes_client).to receive(:cleanup_by_release).with(release_name: releases_names, wait: false)
+
+ subject
+ end
+
+ it 'deletes the associated k8s namespace' do
+ expect(kubernetes_client).to receive(:delete_namespaces_by_exact_names).with(
+ resource_names: releases_names, wait: false
+ )
+
+ subject
+ end
+ end
+
+ shared_examples 'does not delete the helm release' do
+ it 'does not delete the helm release' do
+ expect(helm_client).not_to receive(:delete)
+
+ subject
+ end
+
+ it 'does not empty the k8s resources in the k8s namespace for the release' do
+ expect(kubernetes_client).not_to receive(:cleanup_by_release)
+
+ subject
+ end
+
+ it 'does not delete the associated k8s namespace' do
+ expect(kubernetes_client).not_to receive(:delete_namespaces_by_exact_names)
+
+ subject
+ end
+ end
+
+ shared_examples 'does nothing on a dry run' do
+ it_behaves_like 'does not delete the helm release'
+ end
+
+ it_behaves_like 'the days argument is an integer in the correct range'
+
+ context 'when the helm release is not a review-app release' do
+ let(:helm_releases) do
+ [
+ Tooling::Helm3Client::Release.new(
+ name: 'review-apps', namespace: 'review-apps', revision: 1, status: 'success', updated: three_days_ago.to_s
+ )
+ ]
+ end
+
+ it_behaves_like 'does not delete the helm release'
+ end
+
+ context 'when the helm release is a review-app release' do
+ let(:helm_releases) do
+ [
+ Tooling::Helm3Client::Release.new(
+ name: 'review-test', namespace: 'review-test', revision: 1, status: status, updated: updated_at
+ )
+ ]
+ end
+
+ context 'when the helm release was deployed recently enough' do
+ let(:updated_at) { one_day_ago.to_s }
+
+ context 'when the helm release is in failed state' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'deletes the helm release'
+
+ context 'when the dry-run flag is true' do
+ let(:dry_run) { true }
+
+ it_behaves_like 'does nothing on a dry run'
+ end
+ end
+
+ context 'when the helm release is not in failed state' do
+ let(:status) { 'success' }
+
+ it_behaves_like 'does not delete the helm release'
+ end
+ end
+
+ context 'when the helm release was deployed a while ago' do
+ let(:updated_at) { three_days_ago.to_s }
+
+ context 'when the helm release is in failed state' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'deletes the helm release'
+ end
+
+ context 'when the helm release is not in failed state' do
+ let(:status) { 'success' }
+
+ it_behaves_like 'deletes the helm release'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_entity_spec.rb b/spec/serializers/admin/abuse_report_entity_spec.rb
new file mode 100644
index 00000000000..7d18310977c
--- /dev/null
+++ b/spec/serializers/admin/abuse_report_entity_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
+ let_it_be(:abuse_report) { build_stubbed(:abuse_report) }
+
+ let(:entity) do
+ described_class.new(abuse_report)
+ end
+
+ describe '#as_json' do
+ subject(:entity_hash) { entity.as_json }
+
+ it 'exposes correct attributes' do
+ expect(entity_hash.keys).to include(
+ :category,
+ :updated_at,
+ :reported_user,
+ :reporter
+ )
+ end
+
+ it 'correctly exposes `reported user`' do
+ expect(entity_hash[:reported_user].keys).to match_array([:name])
+ end
+
+ it 'correctly exposes `reporter`' do
+ expect(entity_hash[:reporter].keys).to match_array([:name])
+ end
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_serializer_spec.rb b/spec/serializers/admin/abuse_report_serializer_spec.rb
new file mode 100644
index 00000000000..5b9c229584b
--- /dev/null
+++ b/spec/serializers/admin/abuse_report_serializer_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Admin::AbuseReportSerializer, feature_category: :insider_threat do
+ let(:resource) { build(:abuse_report) }
+
+ subject { described_class.new.represent(resource) }
+
+ describe '#represent' do
+ it 'serializes an abuse report' do
+ expect(subject[:id]).to eq resource.id
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:resource) { build_list(:abuse_report, 2) }
+
+ it 'serializers the array of abuse reports' do
+ expect(subject).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index 916798c669c..ea3826f903a 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -285,7 +285,7 @@ RSpec.describe BuildDetailsEntity do
end
context 'when the build has non public archive type artifacts' do
- let(:build) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:build) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it 'does not expose non public artifacts' do
expect(subject.keys).not_to include(:artifact)
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
deleted file mode 100644
index 1e71e45948c..00000000000
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ClusterApplicationEntity do
- describe '#as_json' do
- let(:application) { build(:clusters_applications_helm, version: '0.1.1') }
-
- subject { described_class.new(application).as_json }
-
- it 'has name' do
- expect(subject[:name]).to eq(application.name)
- end
-
- it 'has status' do
- expect(subject[:status]).to eq(:not_installable)
- end
-
- it 'has version' do
- expect(subject[:version]).to eq('0.1.1')
- end
-
- it 'has no status_reason' do
- expect(subject[:status_reason]).to be_nil
- end
-
- it 'has can_uninstall' do
- expect(subject[:can_uninstall]).to be_truthy
- end
-
- context 'non-helm application' do
- let(:application) { build(:clusters_applications_runner, version: '0.0.0') }
-
- it 'has update_available' do
- expect(subject[:update_available]).to be_truthy
- end
- end
-
- context 'when application is errored' do
- let(:application) { build(:clusters_applications_helm, :errored) }
-
- it 'has corresponded data' do
- expect(subject[:status]).to eq(:errored)
- expect(subject[:status_reason]).not_to be_nil
- expect(subject[:status_reason]).to eq(application.status_reason)
- end
- end
-
- context 'for ingress application' do
- let(:application) do
- build(
- :clusters_applications_ingress,
- :installed,
- external_ip: '111.222.111.222'
- )
- end
-
- it 'includes external_ip' do
- expect(subject[:external_ip]).to eq('111.222.111.222')
- end
- end
-
- context 'for knative application' do
- let(:pages_domain) { create(:pages_domain, :instance_serverless) }
- let(:application) { build(:clusters_applications_knative, :installed) }
-
- before do
- create(:serverless_domain_cluster, knative: application, pages_domain: pages_domain)
- end
-
- it 'includes available domains' do
- expect(subject[:available_domains].length).to eq(1)
- expect(subject[:available_domains].first).to eq(id: pages_domain.id, domain: pages_domain.domain)
- end
-
- it 'includes pages_domain' do
- expect(subject[:pages_domain]).to eq(id: pages_domain.id, domain: pages_domain.domain)
- end
- end
- end
-end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 2de27deeffe..ff1a9a4feac 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -41,19 +41,5 @@ RSpec.describe ClusterEntity do
expect(subject[:status_reason]).to be_nil
end
end
-
- context 'when no application has been installed' do
- let(:cluster) { create(:cluster, :instance) }
-
- subject { described_class.new(cluster, request: request).as_json[:applications] }
-
- it 'contains helm as not_installable' do
- expect(subject).not_to be_empty
-
- helm = subject[0]
- expect(helm[:name]).to eq('helm')
- expect(helm[:status]).to eq(:not_installable)
- end
- end
end
end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 7ec6d3c8bb8..cf102e11b90 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -34,13 +34,13 @@ RSpec.describe ClusterSerializer do
end
it 'serializes attrs correctly' do
- is_expected.to contain_exactly(:status, :status_reason, :applications)
+ is_expected.to contain_exactly(:status, :status_reason)
end
end
context 'when provider type is user' do
it 'serializes attrs correctly' do
- is_expected.to contain_exactly(:status, :status_reason, :applications)
+ is_expected.to contain_exactly(:status, :status_reason)
end
end
end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
index 5a4571339b3..70094991c09 100644
--- a/spec/serializers/entity_date_helper_spec.rb
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -47,8 +47,10 @@ RSpec.describe EntityDateHelper do
end
describe '#remaining_days_in_words' do
+ let(:current_time) { Time.utc(2017, 3, 17) }
+
around do |example|
- travel_to(Time.utc(2017, 3, 17)) { example.run }
+ travel_to(current_time) { example.run }
end
context 'when less than 31 days remaining' do
@@ -74,10 +76,10 @@ RSpec.describe EntityDateHelper do
expect(milestone_remaining).to eq("<strong>1</strong> day remaining")
end
- it 'returns 1 day remaining when queried mid-day' do
- travel_back
+ context 'when queried mid-day' do
+ let(:current_time) { Time.utc(2017, 3, 17, 13, 10) }
- travel_to(Time.utc(2017, 3, 17, 13, 10)) do
+ it 'returns 1 day remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> day remaining")
end
end
diff --git a/spec/serializers/group_issuable_autocomplete_entity_spec.rb b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
index 86ef9dea23b..977239c67da 100644
--- a/spec/serializers/group_issuable_autocomplete_entity_spec.rb
+++ b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe GroupIssuableAutocompleteEntity do
let(:group) { build_stubbed(:group) }
- let(:project) { build_stubbed(:project, group: group) }
+ let(:project_namespace) { build_stubbed(:project_namespace) }
+ let(:project) { build_stubbed(:project, group: group, project_namespace: project_namespace) }
let(:issue) { build_stubbed(:issue, project: project) }
describe '#represent' do
diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb
index 0c9c8f05e17..75aee7f04f0 100644
--- a/spec/serializers/issue_board_entity_spec.rb
+++ b/spec/serializers/issue_board_entity_spec.rb
@@ -57,16 +57,6 @@ RSpec.describe IssueBoardEntity do
context 'when issue is of type task' do
let(:resource) { create(:issue, :task, project: project) }
- context 'when the use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'has a work item path' do
- expect(subject[:real_path]).to eq(project_work_items_path(project, resource.id))
- end
- end
-
it 'has a work item path with iid' do
expect(subject[:real_path]).to eq(project_work_items_path(project, resource.iid, iid_path: true))
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index 06d8523b2e7..795cc357a67 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -17,17 +17,7 @@ RSpec.describe IssueEntity do
context 'when issue is of type task' do
let(:resource) { create(:issue, :task, project: project) }
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- # This was already a path and not a url when the work items change was introduced
- it 'has a work item path' do
- expect(subject[:web_url]).to eq(project_work_items_path(project, resource.id))
- end
- end
-
+ # This was already a path and not a url when the work items change was introduced
it 'has a work item path with iid' do
expect(subject[:web_url]).to eq(project_work_items_path(project, resource.iid, iid_path: true))
end
diff --git a/spec/serializers/linked_project_issue_entity_spec.rb b/spec/serializers/linked_project_issue_entity_spec.rb
index d415d1cbcb2..b863ccde40e 100644
--- a/spec/serializers/linked_project_issue_entity_spec.rb
+++ b/spec/serializers/linked_project_issue_entity_spec.rb
@@ -47,16 +47,6 @@ RSpec.describe LinkedProjectIssueEntity do
context 'when related issue is a task' do
let_it_be(:issue_link) { create(:issue_link, target: create(:issue, :task)) }
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'returns a work items path' do
- expect(serialized_entity).to include(path: project_work_items_path(related_issue.project, related_issue.id))
- end
- end
-
it 'returns a work items path using iid' do
expect(serialized_entity).to include(
path: project_work_items_path(related_issue.project, related_issue.iid, iid_path: true)
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index de05d2afd2b..b4cc0b4db36 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PipelineDetailsEntity do
+RSpec.describe PipelineDetailsEntity, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let(:request) { double('request') }
@@ -32,7 +32,7 @@ RSpec.describe PipelineDetailsEntity do
expect(subject[:details])
.to include :duration, :finished_at
expect(subject[:details])
- .to include :stages, :manual_actions, :scheduled_actions
+ .to include :stages, :manual_actions, :has_manual_actions, :scheduled_actions, :has_scheduled_actions
expect(subject[:details][:status]).to include :icon, :favicon, :text, :label
end
@@ -44,6 +44,30 @@ RSpec.describe PipelineDetailsEntity do
end
end
+ context 'when disable_manual_and_scheduled_actions is true' do
+ let(:pipeline) { create(:ci_pipeline, status: :success) }
+ let(:subject) do
+ described_class.represent(pipeline, request: request, disable_manual_and_scheduled_actions: true).as_json
+ end
+
+ it 'does not contain manual and scheduled actions' do
+ expect(subject[:details])
+ .not_to include :manual_actions, :scheduled_actions
+ end
+ end
+
+ context 'when pipeline has manual builds' do
+ let(:pipeline) { create(:ci_pipeline, status: :success) }
+
+ before do
+ create(:ci_build, :manual, pipeline: pipeline)
+ end
+
+ it 'sets :has_manual_actions to true' do
+ expect(subject[:details][:has_manual_actions]).to eq true
+ end
+ end
+
context 'when pipeline is retryable' do
let(:project) { create(:project) }
diff --git a/spec/serializers/profile/event_entity_spec.rb b/spec/serializers/profile/event_entity_spec.rb
new file mode 100644
index 00000000000..1551fc76466
--- /dev/null
+++ b/spec/serializers/profile/event_entity_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Profile::EventEntity, feature_category: :user_profile do
+ let_it_be(:group) { create(:group) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ let_it_be(:project) { build(:project_empty_repo, group: group) }
+ let_it_be(:user) { create(:user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+
+ let(:target_user) { user }
+ let(:event) { build(:event, :merged, author: user, project: project, target: merge_request) }
+ let(:request) { double(described_class, current_user: user, target_user: target_user) } # rubocop:disable RSpec/VerifiedDoubles
+ let(:entity) { described_class.new(event, request: request) }
+
+ subject { entity.as_json }
+
+ before do
+ group.add_maintainer(user)
+ end
+
+ it 'exposes fields', :aggregate_failures do
+ expect(subject[:created_at]).to eq(event.created_at)
+ expect(subject[:action]).to eq(event.action)
+ expect(subject[:author][:id]).to eq(target_user.id)
+ expect(subject[:author][:name]).to eq(target_user.name)
+ expect(subject[:author][:path]).to eq(target_user.username)
+ end
+
+ context 'for push events' do
+ let_it_be(:commit_from) { Gitlab::Git::BLANK_SHA }
+ let_it_be(:commit_title) { 'My commit' }
+ let(:event) { build(:push_event, project: project, author: target_user) }
+
+ it 'exposes ref fields' do
+ build(:push_event_payload, event: event, ref_count: 3)
+
+ expect(subject[:ref][:type]).to eq(event.ref_type)
+ expect(subject[:ref][:count]).to eq(event.ref_count)
+ expect(subject[:ref][:name]).to eq(event.ref_name)
+ expect(subject[:ref][:path]).to be_nil
+ end
+
+ shared_examples 'returns ref path' do
+ specify do
+ expect(subject[:ref][:path]).to be_present
+ end
+ end
+
+ context 'with tag' do
+ before do
+ allow(project.repository).to receive(:tag_exists?).and_return(true)
+ build(:push_event_payload, event: event, ref_type: :tag)
+ end
+
+ it_behaves_like 'returns ref path'
+ end
+
+ context 'with branch' do
+ before do
+ allow(project.repository).to receive(:branch_exists?).and_return(true)
+ build(:push_event_payload, event: event, ref_type: :branch)
+ end
+
+ it_behaves_like 'returns ref path'
+ end
+
+ it 'exposes commit fields' do
+ build(:push_event_payload, event: event, commit_title: commit_title, commit_from: commit_from, commit_count: 2)
+
+ compare_path = "/#{group.path}/#{project.path}/-/compare/#{commit_from}...#{event.commit_to}"
+ expect(subject[:commit][:compare_path]).to eq(compare_path)
+ expect(event.commit_id).to include(subject[:commit][:truncated_sha])
+ expect(subject[:commit][:path]).to be_present
+ expect(subject[:commit][:title]).to eq(commit_title)
+ expect(subject[:commit][:count]).to eq(2)
+ expect(commit_from).to include(subject[:commit][:from_truncated_sha])
+ expect(event.commit_to).to include(subject[:commit][:to_truncated_sha])
+ expect(subject[:commit][:create_mr_path]).to be_nil
+ end
+
+ it 'exposes create_mr_path' do
+ allow(project).to receive(:default_branch).and_return('main')
+ allow(project.repository).to receive(:branch_exists?).and_return(true)
+ build(:push_event_payload, event: event, action: :created, commit_from: commit_from, commit_count: 2)
+
+ new_mr_path = "/#{group.path}/#{project.path}/-/merge_requests/new?" \
+ "merge_request%5Bsource_branch%5D=#{event.branch_name}"
+ expect(subject[:commit][:create_mr_path]).to eq(new_mr_path)
+ end
+ end
+
+ context 'with target' do
+ let_it_be(:note) { build(:note_on_merge_request, :with_attachment, noteable: merge_request, project: project) }
+
+ context 'when target does not responds to :reference_link_text' do
+ let(:event) { build(:event, :commented, project: project, target: note, author: target_user) }
+
+ it 'exposes target fields' do
+ expect(subject[:target]).not_to include(:reference_link_text)
+ expect(subject[:target][:target_type]).to eq(note.class.to_s)
+ expect(subject[:target][:target_url]).to be_present
+ expect(subject[:target][:title]).to eq(note.title)
+ expect(subject[:target][:first_line_in_markdown]).to be_present
+ expect(subject[:target][:attachment][:url]).to eq(note.attachment.url)
+ end
+ end
+
+ context 'when target responds to :reference_link_text' do
+ it 'exposes reference_link_text' do
+ expect(subject[:target][:reference_link_text]).to eq(merge_request.reference_link_text)
+ end
+ end
+ end
+
+ context 'with resource parent' do
+ it 'exposes resource parent fields' do
+ resource_parent = event.resource_parent
+
+ expect(subject[:resource_parent][:type]).to eq('project')
+ expect(subject[:resource_parent][:full_name]).to eq(resource_parent.full_name)
+ expect(subject[:resource_parent][:full_path]).to eq(resource_parent.full_path)
+ end
+ end
+
+ context 'for private events' do
+ let(:event) { build(:event, :merged, author: target_user) }
+
+ context 'when include_private_contributions? is true' do
+ let(:target_user) { build(:user, include_private_contributions: true) }
+
+ it 'exposes only created_at, action, and author', :aggregate_failures do
+ expect(subject[:created_at]).to eq(event.created_at)
+ expect(subject[:action]).to eq('private')
+ expect(subject[:author][:id]).to eq(target_user.id)
+ expect(subject[:author][:name]).to eq(target_user.name)
+ expect(subject[:author][:path]).to eq(target_user.username)
+
+ is_expected.not_to include(:ref, :commit, :target, :resource_parent)
+ end
+ end
+
+ context 'when include_private_contributions? is false' do
+ let(:target_user) { build(:user, include_private_contributions: false) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb
index 6d292d18ae7..521d0127dbb 100644
--- a/spec/serializers/project_import_entity_spec.rb
+++ b/spec/serializers/project_import_entity_spec.rb
@@ -5,10 +5,11 @@ require 'spec_helper'
RSpec.describe ProjectImportEntity, feature_category: :importers do
include ImportHelper
- let_it_be(:project) { create(:project, import_status: :started, import_source: 'namespace/project') }
+ let_it_be(:project) { create(:project, import_status: :started, import_source: 'import_user/project') }
let(:provider_url) { 'https://provider.com' }
- let(:entity) { described_class.represent(project, provider_url: provider_url) }
+ let(:client) { nil }
+ let(:entity) { described_class.represent(project, provider_url: provider_url, client: client) }
before do
create(:import_failure, project: project)
@@ -23,6 +24,31 @@ RSpec.describe ProjectImportEntity, feature_category: :importers do
expect(subject[:human_import_status_name]).to eq(project.human_import_status_name)
expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source]))
expect(subject[:import_error]).to eq(nil)
+ expect(subject[:relation_type]).to eq(nil)
+ end
+
+ context 'when client option present', :clean_gitlab_redis_cache do
+ let(:octokit) { instance_double(Octokit::Client, access_token: 'stub') }
+ let(:client) do
+ instance_double(
+ ::Gitlab::GithubImport::Clients::Proxy,
+ user: { login: 'import_user' }, octokit: octokit
+ )
+ end
+
+ it 'includes relation_type' do
+ expect(subject[:relation_type]).to eq('owned')
+ end
+
+ context 'with remove_legacy_github_client FF is disabled' do
+ before do
+ stub_feature_flags(remove_legacy_github_client: false)
+ end
+
+ it "doesn't include relation_type" do
+ expect(subject[:relation_type]).to eq(nil)
+ end
+ end
end
context 'when import is failed' do
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb
index 2bf74d64dc9..4cdce094358 100644
--- a/spec/services/access_token_validation_service_spec.rb
+++ b/spec/services/access_token_validation_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AccessTokenValidationService do
+RSpec.describe AccessTokenValidationService, feature_category: :system_access do
describe ".include_any_scope?" do
let(:request) { double("request") }
diff --git a/spec/services/achievements/award_service_spec.rb b/spec/services/achievements/award_service_spec.rb
new file mode 100644
index 00000000000..fb45a634ddd
--- /dev/null
+++ b/spec/services/achievements/award_service_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Achievements::AwardService, feature_category: :user_profile do
+ describe '#execute' do
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:recipient) { create(:user) }
+
+ let(:achievement_id) { achievement.id }
+ let(:recipient_id) { recipient.id }
+
+ subject(:response) { described_class.new(current_user, achievement_id, recipient_id).execute }
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when user does not have permission' do
+ let(:current_user) { developer }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ expect(response.message).to match_array(
+ ['You have insufficient permissions to award this achievement'])
+ end
+ end
+
+ context 'when user has permission' do
+ let(:current_user) { maintainer }
+
+ it 'creates an achievement' do
+ expect(response).to be_success
+ end
+
+ context 'when the achievement is not persisted' do
+ let(:user_achievement) { instance_double('Achievements::UserAchievement') }
+
+ it 'returns the correct error' do
+ allow(user_achievement).to receive(:persisted?).and_return(false)
+ allow(user_achievement).to receive(:errors).and_return(nil)
+ allow(Achievements::UserAchievement).to receive(:create).and_return(user_achievement)
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Failed to award achievement"])
+ end
+ end
+
+ context 'when the achievement does not exist' do
+ let(:achievement_id) { non_existing_record_id }
+
+ it 'returns the correct error' do
+ expect(response).to be_error
+ expect(response.message)
+ .to contain_exactly("Couldn't find Achievements::Achievement with 'id'=#{non_existing_record_id}")
+ end
+ end
+
+ context 'when the recipient does not exist' do
+ let(:recipient_id) { non_existing_record_id }
+
+ it 'returns the correct error' do
+ expect(response).to be_error
+ expect(response.message).to contain_exactly("Couldn't find User with 'id'=#{non_existing_record_id}")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/achievements/revoke_service_spec.rb b/spec/services/achievements/revoke_service_spec.rb
new file mode 100644
index 00000000000..c9925f1e3df
--- /dev/null
+++ b/spec/services/achievements/revoke_service_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Achievements::RevokeService, feature_category: :user_profile do
+ describe '#execute' do
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ let(:user_achievement_param) { user_achievement }
+
+ subject(:response) { described_class.new(current_user, user_achievement_param).execute }
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when user does not have permission' do
+ let(:current_user) { developer }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ expect(response.message).to match_array(
+ ['You have insufficient permissions to revoke this achievement'])
+ end
+ end
+
+ context 'when user has permission' do
+ let(:current_user) { maintainer }
+
+ it 'revokes an achievement' do
+ expect(response).to be_success
+ end
+
+ context 'when the achievement has already been revoked' do
+ let_it_be(:revoked_achievement) { create(:user_achievement, :revoked, achievement: achievement) }
+ let(:user_achievement_param) { revoked_achievement }
+
+ it 'returns the correct error' do
+ expect(response).to be_error
+ expect(response.message)
+ .to contain_exactly('This achievement has already been revoked')
+ end
+ end
+
+ context 'when the user achievement fails to save' do
+ let(:user_achievement_param) { instance_double('Achievements::UserAchievement') }
+
+ it 'returns the correct error' do
+ allow(user_achievement_param).to receive(:save).and_return(false)
+ allow(user_achievement_param).to receive(:achievement).and_return(achievement)
+ allow(user_achievement_param).to receive(:revoked?).and_return(false)
+ allow(user_achievement_param).to receive(:errors).and_return(nil)
+ expect(user_achievement_param).to receive(:assign_attributes)
+
+ expect(response).to be_error
+ expect(response.message).to match_array(["Failed to revoke achievement"])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/admin/set_feature_flag_service_spec.rb b/spec/services/admin/set_feature_flag_service_spec.rb
index 45ee914558a..e66802f6332 100644
--- a/spec/services/admin/set_feature_flag_service_spec.rb
+++ b/spec/services/admin/set_feature_flag_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::SetFeatureFlagService do
+RSpec.describe Admin::SetFeatureFlagService, feature_category: :feature_flags do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/alert_management/alerts/todo/create_service_spec.rb b/spec/services/alert_management/alerts/todo/create_service_spec.rb
index fa4fd8ed0b2..fd81c0893ed 100644
--- a/spec/services/alert_management/alerts/todo/create_service_spec.rb
+++ b/spec/services/alert_management/alerts/todo/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::Alerts::Todo::CreateService do
+RSpec.describe AlertManagement::Alerts::Todo::CreateService, feature_category: :incident_management do
let_it_be(:user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert) }
diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb
index 8375c8cdf7d..69e2f2de291 100644
--- a/spec/services/alert_management/alerts/update_service_spec.rb
+++ b/spec/services/alert_management/alerts/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::Alerts::UpdateService do
+RSpec.describe AlertManagement::Alerts::UpdateService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:other_user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
diff --git a/spec/services/alert_management/create_alert_issue_service_spec.rb b/spec/services/alert_management/create_alert_issue_service_spec.rb
index 7255a722d26..b8d93f99ae4 100644
--- a/spec/services/alert_management/create_alert_issue_service_spec.rb
+++ b/spec/services/alert_management/create_alert_issue_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::CreateAlertIssueService do
+RSpec.describe AlertManagement::CreateAlertIssueService, feature_category: :incident_management do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/services/alert_management/http_integrations/create_service_spec.rb b/spec/services/alert_management/http_integrations/create_service_spec.rb
index ac5c62caf84..5200ec27dd1 100644
--- a/spec/services/alert_management/http_integrations/create_service_spec.rb
+++ b/spec/services/alert_management/http_integrations/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::HttpIntegrations::CreateService do
+RSpec.describe AlertManagement::HttpIntegrations::CreateService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be_with_reload(:project) { create(:project) }
diff --git a/spec/services/alert_management/http_integrations/destroy_service_spec.rb b/spec/services/alert_management/http_integrations/destroy_service_spec.rb
index cd949d728de..a8e9746cb85 100644
--- a/spec/services/alert_management/http_integrations/destroy_service_spec.rb
+++ b/spec/services/alert_management/http_integrations/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::HttpIntegrations::DestroyService do
+RSpec.describe AlertManagement::HttpIntegrations::DestroyService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/alert_management/http_integrations/update_service_spec.rb b/spec/services/alert_management/http_integrations/update_service_spec.rb
index 94c34d9a29c..3f1a0967aa9 100644
--- a/spec/services/alert_management/http_integrations/update_service_spec.rb
+++ b/spec/services/alert_management/http_integrations/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::HttpIntegrations::UpdateService do
+RSpec.describe AlertManagement::HttpIntegrations::UpdateService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/alert_management/metric_images/upload_service_spec.rb b/spec/services/alert_management/metric_images/upload_service_spec.rb
index 527d9db0fd9..2cafd2c9029 100644
--- a/spec/services/alert_management/metric_images/upload_service_spec.rb
+++ b/spec/services/alert_management/metric_images/upload_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::MetricImages::UploadService do
+RSpec.describe AlertManagement::MetricImages::UploadService, feature_category: :metrics do
subject(:service) { described_class.new(alert, current_user, params) }
let_it_be_with_refind(:project) { create(:project) }
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
index ae52a09be48..eb5f3808021 100644
--- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb
+++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AlertManagement::ProcessPrometheusAlertService do
+RSpec.describe AlertManagement::ProcessPrometheusAlertService, feature_category: :incident_management do
let_it_be(:project, reload: true) { create(:project, :repository) }
let(:service) { described_class.new(project, payload) }
diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
index 7bfae0cd9fc..c39965a2799 100644
--- a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
+++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Analytics::CycleAnalytics::Stages::ListService do
+RSpec.describe Analytics::CycleAnalytics::Stages::ListService, feature_category: :value_stream_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:project_namespace) { project.project_namespace.reload }
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index e20d59fb0ef..79d4fc67538 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -110,7 +110,7 @@ RSpec.describe ApplicationSettings::UpdateService do
end
end
- describe 'markdown cache invalidators' do
+ describe 'markdown cache invalidators', feature_category: :team_planning do
shared_examples 'invalidates markdown cache' do |attribute|
let(:params) { attribute }
@@ -144,7 +144,7 @@ RSpec.describe ApplicationSettings::UpdateService do
end
end
- describe 'performance bar settings' do
+ describe 'performance bar settings', feature_category: :application_performance do
using RSpec::Parameterized::TableSyntax
where(:params_performance_bar_enabled,
@@ -247,7 +247,7 @@ RSpec.describe ApplicationSettings::UpdateService do
end
end
- context 'when external authorization is enabled' do
+ context 'when external authorization is enabled', feature_category: :system_access do
before do
enable_external_authorization_service_check
end
diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb
index 4f8b90fcb4a..8a20f7775eb 100644
--- a/spec/services/audit_event_service_spec.rb
+++ b/spec/services/audit_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuditEventService, :with_license do
+RSpec.describe AuditEventService, :with_license, feature_category: :audit_events do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, :with_sign_ins) }
let_it_be(:project_member) { create(:project_member, user: user) }
diff --git a/spec/services/audit_events/build_service_spec.rb b/spec/services/audit_events/build_service_spec.rb
index caf405a53aa..575ec9e58b8 100644
--- a/spec/services/audit_events/build_service_spec.rb
+++ b/spec/services/audit_events/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuditEvents::BuildService do
+RSpec.describe AuditEvents::BuildService, feature_category: :audit_events do
let(:author) { build_stubbed(:author, current_sign_in_ip: '127.0.0.1') }
let(:deploy_token) { build_stubbed(:deploy_token, user: author) }
let(:scope) { build_stubbed(:group) }
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index ba7acd3d3df..90aba1ae54c 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Auth::ContainerRegistryAuthenticationService do
+RSpec.describe Auth::ContainerRegistryAuthenticationService, feature_category: :container_registry do
include AdminModeHelper
it_behaves_like 'a container registry auth service'
diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb
index 667f361dc34..8f92fbe272c 100644
--- a/spec/services/auth/dependency_proxy_authentication_service_spec.rb
+++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Auth::DependencyProxyAuthenticationService do
+RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :dependency_proxy do
let_it_be(:user) { create(:user) }
let(:service) { Auth::DependencyProxyAuthenticationService.new(nil, user) }
diff --git a/spec/services/authorized_project_update/find_records_due_for_refresh_service_spec.rb b/spec/services/authorized_project_update/find_records_due_for_refresh_service_spec.rb
index 691fb3f60f4..e8f86b4d7c5 100644
--- a/spec/services/authorized_project_update/find_records_due_for_refresh_service_spec.rb
+++ b/spec/services/authorized_project_update/find_records_due_for_refresh_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService do
+RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService, feature_category: :projects do
# We're using let! here so that any expectations for the service class are not
# triggered twice.
let!(:project) { create(:project) }
diff --git a/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb
index 782f6858870..51cab6d188b 100644
--- a/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb
+++ b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::PeriodicRecalculateService do
+RSpec.describe AuthorizedProjectUpdate::PeriodicRecalculateService, feature_category: :projects do
subject(:service) { described_class.new }
describe '#execute' do
diff --git a/spec/services/authorized_project_update/project_access_changed_service_spec.rb b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
index da428bece20..7c09d7755ca 100644
--- a/spec/services/authorized_project_update/project_access_changed_service_spec.rb
+++ b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
+RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService, feature_category: :projects do
describe '#execute' do
it 'executes projects_authorizations refresh' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
diff --git a/spec/services/authorized_project_update/project_recalculate_per_user_service_spec.rb b/spec/services/authorized_project_update/project_recalculate_per_user_service_spec.rb
index 62862d0e558..7b2dd52810f 100644
--- a/spec/services/authorized_project_update/project_recalculate_per_user_service_spec.rb
+++ b/spec/services/authorized_project_update/project_recalculate_per_user_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::ProjectRecalculatePerUserService, '#execute' do
+RSpec.describe AuthorizedProjectUpdate::ProjectRecalculatePerUserService, '#execute', feature_category: :projects do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:another_user) { create(:user) }
diff --git a/spec/services/authorized_project_update/project_recalculate_service_spec.rb b/spec/services/authorized_project_update/project_recalculate_service_spec.rb
index c339faaeabf..8360f3c67ab 100644
--- a/spec/services/authorized_project_update/project_recalculate_service_spec.rb
+++ b/spec/services/authorized_project_update/project_recalculate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateService, '#execute' do
+RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateService, '#execute', feature_category: :projects do
let_it_be(:project) { create(:project) }
subject(:execute) { described_class.new(project).execute }
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
index 6c804a14620..7afe5d406ba 100644
--- a/spec/services/auto_merge/base_service_spec.rb
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AutoMerge::BaseService do
+RSpec.describe AutoMerge::BaseService, feature_category: :code_review_workflow do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user, params) }
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 676f55be28a..a0b22267960 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
+RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService, feature_category: :code_review_workflow do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 7584e44152e..94f4b414dca 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AutoMergeService do
+RSpec.describe AutoMergeService, feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb
index 0fbb785e2d6..99dbe6dc606 100644
--- a/spec/services/award_emojis/add_service_spec.rb
+++ b/spec/services/award_emojis/add_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AwardEmojis::AddService do
+RSpec.describe AwardEmojis::AddService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:awardable) { create(:note, project: project) }
diff --git a/spec/services/award_emojis/base_service_spec.rb b/spec/services/award_emojis/base_service_spec.rb
index e0c8fd39ad9..f1ee4d1cfb8 100644
--- a/spec/services/award_emojis/base_service_spec.rb
+++ b/spec/services/award_emojis/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AwardEmojis::BaseService do
+RSpec.describe AwardEmojis::BaseService, feature_category: :team_planning do
let(:awardable) { build(:note) }
let(:current_user) { build(:user) }
diff --git a/spec/services/award_emojis/collect_user_emoji_service_spec.rb b/spec/services/award_emojis/collect_user_emoji_service_spec.rb
index bf5aa0eb9ef..d75d5804f93 100644
--- a/spec/services/award_emojis/collect_user_emoji_service_spec.rb
+++ b/spec/services/award_emojis/collect_user_emoji_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AwardEmojis::CollectUserEmojiService do
+RSpec.describe AwardEmojis::CollectUserEmojiService, feature_category: :team_planning do
describe '#execute' do
it 'returns an Array containing the awarded emoji names' do
user = create(:user)
diff --git a/spec/services/award_emojis/copy_service_spec.rb b/spec/services/award_emojis/copy_service_spec.rb
index abb9c65e25d..6c1d7fb21e2 100644
--- a/spec/services/award_emojis/copy_service_spec.rb
+++ b/spec/services/award_emojis/copy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AwardEmojis::CopyService do
+RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do
let_it_be(:from_awardable) do
create(
:issue,
diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb
index f743de7c59e..109bdbfa986 100644
--- a/spec/services/award_emojis/destroy_service_spec.rb
+++ b/spec/services/award_emojis/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AwardEmojis::DestroyService do
+RSpec.describe AwardEmojis::DestroyService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:awardable) { create(:note) }
let_it_be(:project) { awardable.project }
diff --git a/spec/services/award_emojis/toggle_service_spec.rb b/spec/services/award_emojis/toggle_service_spec.rb
index 74e97c66193..61dcc22561f 100644
--- a/spec/services/award_emojis/toggle_service_spec.rb
+++ b/spec/services/award_emojis/toggle_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AwardEmojis::ToggleService do
+RSpec.describe AwardEmojis::ToggleService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:awardable) { create(:note, project: project) }
diff --git a/spec/services/base_container_service_spec.rb b/spec/services/base_container_service_spec.rb
index 1de79eec702..7406f0aea93 100644
--- a/spec/services/base_container_service_spec.rb
+++ b/spec/services/base_container_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BaseContainerService do
+RSpec.describe BaseContainerService, feature_category: :container_registry do
let(:project) { Project.new }
let(:user) { User.new }
diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb
index 18cab2e8e9a..9a731f52b09 100644
--- a/spec/services/base_count_service_spec.rb
+++ b/spec/services/base_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BaseCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe BaseCountService, :use_clean_rails_memory_store_caching, feature_category: :shared do
let(:service) { described_class.new }
describe '#relation_for_count' do
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index f6a9f0903ce..5aaef9d529c 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::CreateService do
+RSpec.describe Boards::CreateService, feature_category: :team_planning do
describe '#execute' do
context 'when board parent is a project' do
let(:parent) { create(:project) }
diff --git a/spec/services/boards/destroy_service_spec.rb b/spec/services/boards/destroy_service_spec.rb
index cd6df832547..feaca3cfcce 100644
--- a/spec/services/boards/destroy_service_spec.rb
+++ b/spec/services/boards/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::DestroyService do
+RSpec.describe Boards::DestroyService, feature_category: :team_planning do
context 'with project board' do
let_it_be(:parent) { create(:project) }
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index c4f1eb093dc..f9a9c338f58 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Issues::CreateService do
+RSpec.describe Boards::Issues::CreateService, feature_category: :team_planning do
describe '#execute' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 1959710bb0c..5e10d1d216c 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Issues::ListService do
+RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 3a25f13762c..9c173f3f86e 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Issues::MoveService do
+RSpec.describe Boards::Issues::MoveService, feature_category: :team_planning do
describe '#execute' do
context 'when parent is a project' do
let(:user) { create(:user) }
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index cac26b3c88d..da317fdd474 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Lists::CreateService do
+RSpec.describe Boards::Lists::CreateService, feature_category: :team_planning do
context 'when board parent is a project' do
let_it_be(:parent) { create(:project) }
let_it_be(:board) { create(:board, project: parent) }
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index d5358bcc1e1..837635d49e8 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Lists::DestroyService do
+RSpec.describe Boards::Lists::DestroyService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let(:list_type) { :list }
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 2d41de42581..90b705e05c3 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Lists::ListService do
+RSpec.describe Boards::Lists::ListService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 2861fc48b4d..abf7d48e114 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Lists::MoveService do
+RSpec.describe Boards::Lists::MoveService, feature_category: :team_planning do
describe '#execute' do
context 'when board parent is a project' do
let(:project) { create(:project) }
diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb
index 21216e1b945..341eaa52292 100644
--- a/spec/services/boards/lists/update_service_spec.rb
+++ b/spec/services/boards/lists/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Lists::UpdateService do
+RSpec.describe Boards::Lists::UpdateService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let!(:list) { create(:list, board: board, position: 0) }
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
index 8910345d170..4af4e914da5 100644
--- a/spec/services/boards/visits/create_service_spec.rb
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Boards::Visits::CreateService do
+RSpec.describe Boards::Visits::CreateService, feature_category: :team_planning do
describe '#execute' do
let(:user) { create(:user) }
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index 19a32aafa38..7fb7d9d440d 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
+RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching, feature_category: :source_code_management do
subject(:service) { described_class.new(project, user) }
let_it_be(:project) { create(:project_empty_repo) }
@@ -108,7 +108,7 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
control = RedisCommands::Recorder.new(pattern: ':branch_names:') { subject }
- expect(control.by_command(:sadd).count).to eq(1)
+ expect(control).not_to exceed_redis_command_calls_limit(:sadd, 1)
end
end
diff --git a/spec/services/branches/delete_merged_service_spec.rb b/spec/services/branches/delete_merged_service_spec.rb
index 46611670fe1..23a892a56ef 100644
--- a/spec/services/branches/delete_merged_service_spec.rb
+++ b/spec/services/branches/delete_merged_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Branches::DeleteMergedService do
+RSpec.describe Branches::DeleteMergedService, feature_category: :source_code_management do
include ProjectForksHelper
subject(:service) { described_class.new(project, project.first_owner) }
diff --git a/spec/services/branches/delete_service_spec.rb b/spec/services/branches/delete_service_spec.rb
index 727cadc5a50..003645b1b8c 100644
--- a/spec/services/branches/delete_service_spec.rb
+++ b/spec/services/branches/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Branches::DeleteService do
+RSpec.describe Branches::DeleteService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
diff --git a/spec/services/branches/diverging_commit_counts_service_spec.rb b/spec/services/branches/diverging_commit_counts_service_spec.rb
index 34a2b81c831..3cccc74735c 100644
--- a/spec/services/branches/diverging_commit_counts_service_spec.rb
+++ b/spec/services/branches/diverging_commit_counts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Branches::DivergingCommitCountsService do
+RSpec.describe Branches::DivergingCommitCountsService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
diff --git a/spec/services/branches/validate_new_service_spec.rb b/spec/services/branches/validate_new_service_spec.rb
index 02127c8c10d..a5b75a09353 100644
--- a/spec/services/branches/validate_new_service_spec.rb
+++ b/spec/services/branches/validate_new_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Branches::ValidateNewService do
+RSpec.describe Branches::ValidateNewService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
subject(:service) { described_class.new(project) }
diff --git a/spec/services/bulk_create_integration_service_spec.rb b/spec/services/bulk_create_integration_service_spec.rb
index 22bb1736f9f..57bdfdbd4cb 100644
--- a/spec/services/bulk_create_integration_service_spec.rb
+++ b/spec/services/bulk_create_integration_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkCreateIntegrationService do
+RSpec.describe BulkCreateIntegrationService, feature_category: :integrations do
include JiraIntegrationHelpers
before_all do
diff --git a/spec/services/bulk_imports/archive_extraction_service_spec.rb b/spec/services/bulk_imports/archive_extraction_service_spec.rb
index da9df31cde9..40f8d8718ae 100644
--- a/spec/services/bulk_imports/archive_extraction_service_spec.rb
+++ b/spec/services/bulk_imports/archive_extraction_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::ArchiveExtractionService do
+RSpec.describe BulkImports::ArchiveExtractionService, feature_category: :importers do
let_it_be(:tmpdir) { Dir.mktmpdir }
let_it_be(:filename) { 'symlink_export.tar' }
let_it_be(:filepath) { File.join(tmpdir, filename) }
diff --git a/spec/services/bulk_imports/export_service_spec.rb b/spec/services/bulk_imports/export_service_spec.rb
index 2414f7c5ca7..ac7514fde5b 100644
--- a/spec/services/bulk_imports/export_service_spec.rb
+++ b/spec/services/bulk_imports/export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::ExportService do
+RSpec.describe BulkImports::ExportService, feature_category: :importers do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/bulk_imports/file_decompression_service_spec.rb b/spec/services/bulk_imports/file_decompression_service_spec.rb
index 77348428d60..9b8320aeac5 100644
--- a/spec/services/bulk_imports/file_decompression_service_spec.rb
+++ b/spec/services/bulk_imports/file_decompression_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileDecompressionService do
+RSpec.describe BulkImports::FileDecompressionService, feature_category: :importers do
let_it_be(:tmpdir) { Dir.mktmpdir }
let_it_be(:ndjson_filename) { 'labels.ndjson' }
let_it_be(:ndjson_filepath) { File.join(tmpdir, ndjson_filename) }
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index 27f77b678e3..7c64d6efc65 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileDownloadService do
+RSpec.describe BulkImports::FileDownloadService, feature_category: :importers do
describe '#execute' do
let_it_be(:allowed_content_types) { %w(application/gzip application/octet-stream) }
let_it_be(:file_size_limit) { 5.gigabytes }
diff --git a/spec/services/bulk_imports/file_export_service_spec.rb b/spec/services/bulk_imports/file_export_service_spec.rb
index 453fc1d0c0d..3c23b86ad5c 100644
--- a/spec/services/bulk_imports/file_export_service_spec.rb
+++ b/spec/services/bulk_imports/file_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::FileExportService do
+RSpec.describe BulkImports::FileExportService, feature_category: :importers do
let_it_be(:project) { create(:project) }
describe '#execute' do
diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
index 894789c7941..4f721a3a259 100644
--- a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
+++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::LfsObjectsExportService do
+RSpec.describe BulkImports::LfsObjectsExportService, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:lfs_json_filename) { "#{BulkImports::FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION}.json" }
let_it_be(:remote_url) { 'http://my-object-storage.local' }
diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb
index f0f85217d2e..bc999b0b9b3 100644
--- a/spec/services/bulk_imports/relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::RelationExportService do
+RSpec.describe BulkImports::RelationExportService, feature_category: :importers do
let_it_be(:jid) { 'jid' }
let_it_be(:relation) { 'labels' }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/bulk_imports/repository_bundle_export_service_spec.rb b/spec/services/bulk_imports/repository_bundle_export_service_spec.rb
index f0d63de1ab9..92d5d21c33f 100644
--- a/spec/services/bulk_imports/repository_bundle_export_service_spec.rb
+++ b/spec/services/bulk_imports/repository_bundle_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::RepositoryBundleExportService do
+RSpec.describe BulkImports::RepositoryBundleExportService, feature_category: :importers do
let(:project) { create(:project) }
let(:export_path) { Dir.mktmpdir }
diff --git a/spec/services/bulk_imports/tree_export_service_spec.rb b/spec/services/bulk_imports/tree_export_service_spec.rb
index 6e26cb6dc2b..fa96641f1c1 100644
--- a/spec/services/bulk_imports/tree_export_service_spec.rb
+++ b/spec/services/bulk_imports/tree_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::TreeExportService do
+RSpec.describe BulkImports::TreeExportService, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:export_path) { Dir.mktmpdir }
diff --git a/spec/services/bulk_imports/uploads_export_service_spec.rb b/spec/services/bulk_imports/uploads_export_service_spec.rb
index ad6e005485c..8dc67b28d12 100644
--- a/spec/services/bulk_imports/uploads_export_service_spec.rb
+++ b/spec/services/bulk_imports/uploads_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::UploadsExportService do
+RSpec.describe BulkImports::UploadsExportService, feature_category: :importers do
let_it_be(:export_path) { Dir.mktmpdir }
let_it_be(:project) { create(:project, avatar: fixture_file_upload('spec/fixtures/rails_sample.png', 'image/png')) }
diff --git a/spec/services/bulk_push_event_payload_service_spec.rb b/spec/services/bulk_push_event_payload_service_spec.rb
index 381c735c003..95e1c831498 100644
--- a/spec/services/bulk_push_event_payload_service_spec.rb
+++ b/spec/services/bulk_push_event_payload_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkPushEventPayloadService do
+RSpec.describe BulkPushEventPayloadService, feature_category: :source_code_management do
let(:event) { create(:push_event) }
let(:push_data) do
diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb
index 24a868b524d..260eed3c734 100644
--- a/spec/services/bulk_update_integration_service_spec.rb
+++ b/spec/services/bulk_update_integration_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkUpdateIntegrationService do
+RSpec.describe BulkUpdateIntegrationService, feature_category: :integrations do
include JiraIntegrationHelpers
before_all do
diff --git a/spec/services/captcha/captcha_verification_service_spec.rb b/spec/services/captcha/captcha_verification_service_spec.rb
index fe2199fb53e..b67e725bf91 100644
--- a/spec/services/captcha/captcha_verification_service_spec.rb
+++ b/spec/services/captcha/captcha_verification_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Captcha::CaptchaVerificationService do
+RSpec.describe Captcha::CaptchaVerificationService, feature_category: :team_planning do
describe '#execute' do
let(:captcha_response) { 'abc123' }
let(:fake_ip) { '1.2.3.4' }
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
index 10cb0a2f065..14bece4efb4 100644
--- a/spec/services/chat_names/find_user_service_spec.rb
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
+RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state, feature_category: :user_profile do
describe '#execute' do
subject { described_class.new(team_id, user_id).execute }
diff --git a/spec/services/ci/abort_pipelines_service_spec.rb b/spec/services/ci/abort_pipelines_service_spec.rb
index e43faf0af51..60f3ee11442 100644
--- a/spec/services/ci/abort_pipelines_service_spec.rb
+++ b/spec/services/ci/abort_pipelines_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::AbortPipelinesService do
+RSpec.describe Ci::AbortPipelinesService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/ci/append_build_trace_service_spec.rb b/spec/services/ci/append_build_trace_service_spec.rb
index 20f7967d1f1..113c88dc5f3 100644
--- a/spec/services/ci/append_build_trace_service_spec.rb
+++ b/spec/services/ci/append_build_trace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::AppendBuildTraceService do
+RSpec.describe Ci::AppendBuildTraceService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be_with_reload(:build) { create(:ci_build, :running, pipeline: pipeline) }
diff --git a/spec/services/ci/build_cancel_service_spec.rb b/spec/services/ci/build_cancel_service_spec.rb
index fe036dc1368..314d2e86bd3 100644
--- a/spec/services/ci/build_cancel_service_spec.rb
+++ b/spec/services/ci/build_cancel_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildCancelService do
+RSpec.describe Ci::BuildCancelService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/build_erase_service_spec.rb b/spec/services/ci/build_erase_service_spec.rb
index e750a163621..35e74f76a26 100644
--- a/spec/services/ci/build_erase_service_spec.rb
+++ b/spec/services/ci/build_erase_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildEraseService do
+RSpec.describe Ci::BuildEraseService, feature_category: :continuous_integration do
let_it_be(:user) { user }
let(:build) { create(:ci_build, :artifacts, :trace_artifact, artifacts_expire_at: 100.days.from_now) }
diff --git a/spec/services/ci/build_report_result_service_spec.rb b/spec/services/ci/build_report_result_service_spec.rb
index c5238b7f5e0..c3ce6714241 100644
--- a/spec/services/ci/build_report_result_service_spec.rb
+++ b/spec/services/ci/build_report_result_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildReportResultService do
+RSpec.describe Ci::BuildReportResultService, feature_category: :continuous_integration do
describe '#execute', :clean_gitlab_redis_shared_state do
subject(:build_report_result) { described_class.new.execute(build) }
diff --git a/spec/services/ci/build_unschedule_service_spec.rb b/spec/services/ci/build_unschedule_service_spec.rb
index d784d9a2754..539c66047e4 100644
--- a/spec/services/ci/build_unschedule_service_spec.rb
+++ b/spec/services/ci/build_unschedule_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildUnscheduleService do
+RSpec.describe Ci::BuildUnscheduleService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/catalog/add_resource_service_spec.rb b/spec/services/ci/catalog/add_resource_service_spec.rb
new file mode 100644
index 00000000000..ecb939e3c2d
--- /dev/null
+++ b/spec/services/ci/catalog/add_resource_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_composition do
+ let_it_be(:project) { create(:project, :repository, description: 'Our components') }
+ let_it_be(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'with an unauthorized user' do
+ it 'raises an AccessDeniedError' do
+ expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'with an authorized user' do
+ before do
+ project.add_owner(user)
+ end
+
+ context 'and a valid project' do
+ it 'creates a catalog resource' do
+ response = service.execute
+
+ expect(response.payload.project).to eq(project)
+ end
+ end
+
+ context 'with an invalid project' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ it 'does not create a catalog resource' do
+ response = service.execute
+
+ expect(response.message).to eq('Project must have a description')
+ end
+ end
+
+ context 'with an invalid catalog resource' do
+ it 'does not save the catalog resource' do
+ catalog_resource = instance_double(::Ci::Catalog::Resource,
+ valid?: false,
+ errors: instance_double(ActiveModel::Errors, full_messages: ['not valid']))
+ allow(::Ci::Catalog::Resource).to receive(:new).and_return(catalog_resource)
+
+ response = service.execute
+
+ expect(response.message).to eq('not valid')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/catalog/validate_resource_service_spec.rb b/spec/services/ci/catalog/validate_resource_service_spec.rb
new file mode 100644
index 00000000000..3bee37b7e55
--- /dev/null
+++ b/spec/services/ci/catalog/validate_resource_service_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Catalog::ValidateResourceService, feature_category: :pipeline_composition do
+ describe '#execute' do
+ context 'with a project that has a README and a description' do
+ it 'is valid' do
+ project = create(:project, :repository, description: 'Component project')
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response).to be_success
+ end
+ end
+
+ context 'with a project that has neither a description nor a README' do
+ it 'is not valid' do
+ project = create(:project, :empty_repo)
+ project.repository.create_file(
+ project.creator,
+ 'ruby.rb',
+ 'I like this',
+ message: 'Ruby like this',
+ branch_name: 'master'
+ )
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq('Project must have a README , Project must have a description')
+ end
+ end
+
+ context 'with a project that has a description but not a README' do
+ it 'is not valid' do
+ project = create(:project, :empty_repo, description: 'project with no README')
+ project.repository.create_file(
+ project.creator,
+ 'text.txt',
+ 'I do not like this',
+ message: 'only text like text',
+ branch_name: 'master'
+ )
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq('Project must have a README')
+ end
+ end
+
+ context 'with a project that has a README and not a description' do
+ it 'is not valid' do
+ project = create(:project, :repository)
+ response = described_class.new(project, project.default_branch).execute
+
+ expect(response.message).to eq('Project must have a description')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/change_variable_service_spec.rb b/spec/services/ci/change_variable_service_spec.rb
index f86a87132b1..a9f9e4233d7 100644
--- a/spec/services/ci/change_variable_service_spec.rb
+++ b/spec/services/ci/change_variable_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ChangeVariableService do
+RSpec.describe Ci::ChangeVariableService, feature_category: :pipeline_composition do
let(:service) { described_class.new(container: group, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ci/change_variables_service_spec.rb b/spec/services/ci/change_variables_service_spec.rb
index b710ca78554..1bc36a78762 100644
--- a/spec/services/ci/change_variables_service_spec.rb
+++ b/spec/services/ci/change_variables_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ChangeVariablesService do
+RSpec.describe Ci::ChangeVariablesService, feature_category: :pipeline_composition do
let(:service) { described_class.new(container: group, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ci/compare_accessibility_reports_service_spec.rb b/spec/services/ci/compare_accessibility_reports_service_spec.rb
index e0b84219834..57514c18042 100644
--- a/spec/services/ci/compare_accessibility_reports_service_spec.rb
+++ b/spec/services/ci/compare_accessibility_reports_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CompareAccessibilityReportsService do
+RSpec.describe Ci::CompareAccessibilityReportsService, feature_category: :continuous_integration do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/ci/compare_codequality_reports_service_spec.rb b/spec/services/ci/compare_codequality_reports_service_spec.rb
index ef762a2e9ad..de2300c354b 100644
--- a/spec/services/ci/compare_codequality_reports_service_spec.rb
+++ b/spec/services/ci/compare_codequality_reports_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CompareCodequalityReportsService do
+RSpec.describe Ci::CompareCodequalityReportsService, feature_category: :continuous_integration do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/ci/compare_reports_base_service_spec.rb b/spec/services/ci/compare_reports_base_service_spec.rb
index 20d8cd37553..2906d61066d 100644
--- a/spec/services/ci/compare_reports_base_service_spec.rb
+++ b/spec/services/ci/compare_reports_base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CompareReportsBaseService do
+RSpec.describe Ci::CompareReportsBaseService, feature_category: :continuous_integration do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/ci/compare_test_reports_service_spec.rb b/spec/services/ci/compare_test_reports_service_spec.rb
index f259072fe87..d29cef0c583 100644
--- a/spec/services/ci/compare_test_reports_service_spec.rb
+++ b/spec/services/ci/compare_test_reports_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CompareTestReportsService do
+RSpec.describe Ci::CompareTestReportsService, feature_category: :continuous_integration do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb
index f2eaa8d31b4..532098b3b20 100644
--- a/spec/services/ci/components/fetch_service_spec.rb
+++ b/spec/services/ci/components/fetch_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_authoring do
+RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') }
let_it_be(:user) { create(:user) }
let_it_be(:current_user) { user }
diff --git a/spec/services/ci/copy_cross_database_associations_service_spec.rb b/spec/services/ci/copy_cross_database_associations_service_spec.rb
index 5938ac258d0..2be0cbd9582 100644
--- a/spec/services/ci/copy_cross_database_associations_service_spec.rb
+++ b/spec/services/ci/copy_cross_database_associations_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CopyCrossDatabaseAssociationsService do
+RSpec.describe Ci::CopyCrossDatabaseAssociationsService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:old_build) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/services/ci/create_pipeline_service/artifacts_spec.rb b/spec/services/ci/create_pipeline_service/artifacts_spec.rb
index e5e405492a0..c193900b2d3 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, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, feature_category: :build_artifacts 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 f9640f99031..e8d9cec6695 100644
--- a/spec/services/ci/create_pipeline_service/cache_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cache_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 0ebcecdd6e6..036116dc037 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, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, feature_category: :continuous_integration 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 0d5017a763f..dc87cc872e3 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,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 dafa227c4c8..819946bfd27 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, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, feature_category: :continuous_integration 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 3b042f05fc0..b78ad68194a 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,8 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 de1ed251c82..7136fa5dc13 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,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 b713cad2cad..96e54af43cd 100644
--- a/spec/services/ci/create_pipeline_service/environment_spec.rb
+++ b/spec/services/ci/create_pipeline_service/environment_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
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 e84726d31f6..b40e504f99b 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,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :pipeline_composition 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 f18b4883aaf..86f71be5971 100644
--- a/spec/services/ci/create_pipeline_service/include_spec.rb
+++ b/spec/services/ci/create_pipeline_service/include_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService,
-:yaml_processor_feature_flag_corectness, feature_category: :pipeline_authoring do
+:yaml_processor_feature_flag_corectness, feature_category: :pipeline_composition do
include RepoHelpers
context 'include:' do
diff --git a/spec/services/ci/create_pipeline_service/limit_active_jobs_spec.rb b/spec/services/ci/create_pipeline_service/limit_active_jobs_spec.rb
index 003d109a27c..b0730eaf215 100644
--- a/spec/services/ci/create_pipeline_service/limit_active_jobs_spec.rb
+++ b/spec/services/ci/create_pipeline_service/limit_active_jobs_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
let_it_be(:existing_pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/create_pipeline_service/logger_spec.rb b/spec/services/ci/create_pipeline_service/logger_spec.rb
index ecb24a61075..082a09db69e 100644
--- a/spec/services/ci/create_pipeline_service/logger_spec.rb
+++ b/spec/services/ci/create_pipeline_service/logger_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe Ci::CreatePipelineService, # rubocop: disable RSpec/FilePath
describe 'pipeline includes count' do
before do
- stub_const('Gitlab::Ci::Config::External::Context::MAX_INCLUDES', 2)
+ stub_const('Gitlab::Ci::Config::External::Context::TEMP_MAX_INCLUDES', 2)
end
context 'when the includes count exceeds the maximum' do
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 80f48451e5c..8d04b3d020f 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,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+feature_category: :continuous_integration 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 38e330316ea..068cad68e64 100644
--- a/spec/services/ci/create_pipeline_service/needs_spec.rb
+++ b/spec/services/ci/create_pipeline_service/needs_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :pipeline_composition 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 5ee378a9719..71434fe0b0c 100644
--- a/spec/services/ci/create_pipeline_service/parallel_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parallel_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 cae88bb67cf..16555dd68d6 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,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 eb17935967c..e644273df9a 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,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
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 db110bdc608..d935824e6cc 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,8 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 dfa74870341..31bea10f062 100644
--- a/spec/services/ci/create_pipeline_service/rate_limit_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rate_limit_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService, :freeze_time,
:clean_gitlab_redis_rate_limiting,
- :yaml_processor_feature_flag_corectness do
+ :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration 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 26bb8b7d006..19f9e7e3e4a 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -1,25 +1,18 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, feature_category: :pipeline_authoring do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, feature_category: :pipeline_composition do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
- let(:service) { described_class.new(project, user, { ref: ref }) }
- let(:response) { execute_service }
+ let(:service) { described_class.new(project, user, initialization_params) }
+ let(:response) { service.execute(source) }
let(:pipeline) { response.payload }
let(:build_names) { pipeline.builds.pluck(:name) }
- def execute_service(before: '00000000', variables_attributes: nil)
- params = { ref: ref, before: before, after: project.commit(ref).sha, variables_attributes: variables_attributes }
-
- described_class
- .new(project, user, params)
- .execute(source) do |pipeline|
- yield(pipeline) if block_given?
- end
- end
+ let(:base_initialization_params) { { ref: ref, before: '00000000', after: project.commit(ref).sha, variables_attributes: nil } }
+ let(:initialization_params) { base_initialization_params }
context 'job:rules' do
let(:regular_job) { find_job('regular-job') }
@@ -516,11 +509,10 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
)
end
+ let(:initialization_params) { base_initialization_params.merge(before: nil) }
let(:changed_file) { 'file2.txt' }
let(:ref) { 'feature_2' }
- let(:response) { execute_service(before: nil) }
-
context 'for jobs rules' do
let(:config) do
<<-EOY
@@ -1230,9 +1222,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
context 'with pipeline variables' do
- let(:pipeline) do
- execute_service(variables_attributes: variables_attributes).payload
- end
+ let(:initialization_params) { base_initialization_params.merge(variables_attributes: variables_attributes) }
let(:config) do
<<-EOY
@@ -1267,10 +1257,10 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
context 'with trigger variables' do
- let(:pipeline) do
- execute_service do |pipeline|
+ let(:response) do
+ service.execute(source) do |pipeline|
pipeline.variables.build(variables)
- end.payload
+ end
end
let(:config) do
@@ -1434,10 +1424,10 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
[{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAL' }]
end
- let(:pipeline) do
- execute_service do |pipeline|
+ let(:response) do
+ service.execute(source) do |pipeline|
pipeline.variables.build(variables)
- end.payload
+ end
end
let(:config) do
diff --git a/spec/services/ci/create_pipeline_service/scripts_spec.rb b/spec/services/ci/create_pipeline_service/scripts_spec.rb
index 50b558e505a..d541257a086 100644
--- a/spec/services/ci/create_pipeline_service/scripts_spec.rb
+++ b/spec/services/ci/create_pipeline_service/scripts_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -83,30 +84,5 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
options: { script: ["echo 'hello job3 script'"] }
)
end
-
- context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
- before do
- stub_feature_flags(ci_hooks_pre_get_sources_script: false)
- end
-
- it 'creates jobs without hook data' do
- expect(pipeline).to be_created_successfully
- expect(pipeline.builds.find_by(name: 'job1')).to have_attributes(
- name: 'job1',
- stage: 'test',
- options: { script: ["echo 'hello job1 script'"] }
- )
- expect(pipeline.builds.find_by(name: 'job2')).to have_attributes(
- name: 'job2',
- stage: 'test',
- options: { script: ["echo 'hello job2 script'"] }
- )
- expect(pipeline.builds.find_by(name: 'job3')).to have_attributes(
- name: 'job3',
- stage: 'test',
- options: { script: ["echo 'hello job3 script'"] }
- )
- end
- end
end
end
diff --git a/spec/services/ci/create_pipeline_service/tags_spec.rb b/spec/services/ci/create_pipeline_service/tags_spec.rb
index 7450df11eac..cb2dbf1c3a4 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, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, feature_category: :continuous_integration do
describe '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/variables_spec.rb b/spec/services/ci/create_pipeline_service/variables_spec.rb
index fd138bde656..64f8b90f2f2 100644
--- a/spec/services/ci/create_pipeline_service/variables_spec.rb
+++ b/spec/services/ci/create_pipeline_service/variables_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness,
+ feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
diff --git a/spec/services/ci/create_web_ide_terminal_service_spec.rb b/spec/services/ci/create_web_ide_terminal_service_spec.rb
index 3462b48cfe7..b22ca1472b7 100644
--- a/spec/services/ci/create_web_ide_terminal_service_spec.rb
+++ b/spec/services/ci/create_web_ide_terminal_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreateWebIdeTerminalService do
+RSpec.describe Ci::CreateWebIdeTerminalService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ci/daily_build_group_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb
index 32651247adb..bb6ce559fbd 100644
--- a/spec/services/ci/daily_build_group_report_result_service_spec.rb
+++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute' do
+RSpec.describe Ci::DailyBuildGroupReportResultService, '#execute', feature_category: :continuous_integration do
let_it_be(:group) { create(:group, :private) }
let_it_be(:pipeline) { create(:ci_pipeline, project: create(:project, group: group), created_at: '2020-02-06 00:01:10') }
let_it_be(:rspec_job) { create(:ci_build, pipeline: pipeline, name: 'rspec 3/3', coverage: 80) }
diff --git a/spec/services/ci/delete_objects_service_spec.rb b/spec/services/ci/delete_objects_service_spec.rb
index 448f8979681..d84ee596721 100644
--- a/spec/services/ci/delete_objects_service_spec.rb
+++ b/spec/services/ci/delete_objects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DeleteObjectsService, :aggregate_failure do
+RSpec.describe Ci::DeleteObjectsService, :aggregate_failure, feature_category: :continuous_integration do
let(:service) { described_class.new }
let(:artifact) { create(:ci_job_artifact, :archive) }
let(:data) { [artifact] }
diff --git a/spec/services/ci/delete_unit_tests_service_spec.rb b/spec/services/ci/delete_unit_tests_service_spec.rb
index 4c63c513d48..2f07e709107 100644
--- a/spec/services/ci/delete_unit_tests_service_spec.rb
+++ b/spec/services/ci/delete_unit_tests_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DeleteUnitTestsService do
+RSpec.describe Ci::DeleteUnitTestsService, feature_category: :continuous_integration do
describe '#execute' do
let!(:unit_test_1) { create(:ci_unit_test) }
let!(:unit_test_2) { create(:ci_unit_test) }
diff --git a/spec/services/ci/deployments/destroy_service_spec.rb b/spec/services/ci/deployments/destroy_service_spec.rb
index 60a57c05728..d0e7f5acb2b 100644
--- a/spec/services/ci/deployments/destroy_service_spec.rb
+++ b/spec/services/ci/deployments/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::Deployments::DestroyService do
+RSpec.describe ::Ci::Deployments::DestroyService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
index 6bd7fe7559c..a1883d90b0a 100644
--- a/spec/services/ci/destroy_pipeline_service_spec.rb
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::DestroyPipelineService do
+RSpec.describe ::Ci::DestroyPipelineService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.id) }
diff --git a/spec/services/ci/destroy_secure_file_service_spec.rb b/spec/services/ci/destroy_secure_file_service_spec.rb
index 6a30d33f4ca..321efc2ed71 100644
--- a/spec/services/ci/destroy_secure_file_service_spec.rb
+++ b/spec/services/ci/destroy_secure_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::DestroySecureFileService do
+RSpec.describe ::Ci::DestroySecureFileService, feature_category: :continuous_integration do
let_it_be(:maintainer_user) { create(:user) }
let_it_be(:developer_user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/ci/disable_user_pipeline_schedules_service_spec.rb b/spec/services/ci/disable_user_pipeline_schedules_service_spec.rb
index 4ff8dcf075b..d422cf0dab9 100644
--- a/spec/services/ci/disable_user_pipeline_schedules_service_spec.rb
+++ b/spec/services/ci/disable_user_pipeline_schedules_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DisableUserPipelineSchedulesService do
+RSpec.describe Ci::DisableUserPipelineSchedulesService, feature_category: :continuous_integration do
describe '#execute' do
let(:user) { create(:user) }
diff --git a/spec/services/ci/drop_pipeline_service_spec.rb b/spec/services/ci/drop_pipeline_service_spec.rb
index ddb53712d9c..ed45b3460c1 100644
--- a/spec/services/ci/drop_pipeline_service_spec.rb
+++ b/spec/services/ci/drop_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DropPipelineService do
+RSpec.describe Ci::DropPipelineService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let(:failure_reason) { :user_blocked }
diff --git a/spec/services/ci/ensure_stage_service_spec.rb b/spec/services/ci/ensure_stage_service_spec.rb
index 026814edda6..5d6025095a1 100644
--- a/spec/services/ci/ensure_stage_service_spec.rb
+++ b/spec/services/ci/ensure_stage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::EnsureStageService, '#execute' do
+RSpec.describe Ci::EnsureStageService, '#execute', feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb
index 8cfe756faf3..3d0ce456aa5 100644
--- a/spec/services/ci/expire_pipeline_cache_service_spec.rb
+++ b/spec/services/ci/expire_pipeline_cache_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ExpirePipelineCacheService do
+RSpec.describe Ci::ExpirePipelineCacheService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb
index d5881d3b204..1b548aaf614 100644
--- a/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb
+++ b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ExternalPullRequests::CreatePipelineService do
+RSpec.describe Ci::ExternalPullRequests::CreatePipelineService, feature_category: :continuous_integration do
describe '#execute' do
let_it_be(:project) { create(:project, :auto_devops, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ci/find_exposed_artifacts_service_spec.rb b/spec/services/ci/find_exposed_artifacts_service_spec.rb
index 6e11c153a75..69360e73b86 100644
--- a/spec/services/ci/find_exposed_artifacts_service_spec.rb
+++ b/spec/services/ci/find_exposed_artifacts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::FindExposedArtifactsService do
+RSpec.describe Ci::FindExposedArtifactsService, feature_category: :build_artifacts do
include Gitlab::Routing
let(:metadata) do
diff --git a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
index 63bc7a1caf8..c33b182e9a9 100644
--- a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
+++ b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::GenerateCodequalityMrDiffReportService do
+RSpec.describe Ci::GenerateCodequalityMrDiffReportService, feature_category: :code_review_workflow do
let(:service) { described_class.new(project) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/ci/generate_coverage_reports_service_spec.rb b/spec/services/ci/generate_coverage_reports_service_spec.rb
index 212e6be9d07..811431bf9d6 100644
--- a/spec/services/ci/generate_coverage_reports_service_spec.rb
+++ b/spec/services/ci/generate_coverage_reports_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::GenerateCoverageReportsService do
+RSpec.describe Ci::GenerateCoverageReportsService, feature_category: :code_testing do
let_it_be(:project) { create(:project, :repository) }
let(:service) { described_class.new(project) }
diff --git a/spec/services/ci/generate_kubeconfig_service_spec.rb b/spec/services/ci/generate_kubeconfig_service_spec.rb
index c0858b0f0c9..da18dfe04c3 100644
--- a/spec/services/ci/generate_kubeconfig_service_spec.rb
+++ b/spec/services/ci/generate_kubeconfig_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::GenerateKubeconfigService do
+RSpec.describe Ci::GenerateKubeconfigService, feature_category: :kubernetes_management do
describe '#execute' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/services/ci/generate_terraform_reports_service_spec.rb b/spec/services/ci/generate_terraform_reports_service_spec.rb
index c32e8bcaeb8..b2142d391b8 100644
--- a/spec/services/ci/generate_terraform_reports_service_spec.rb
+++ b/spec/services/ci/generate_terraform_reports_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::GenerateTerraformReportsService do
+RSpec.describe Ci::GenerateTerraformReportsService, feature_category: :infrastructure_as_code do
let_it_be(:project) { create(:project, :repository) }
describe '#execute' do
diff --git a/spec/services/ci/job_artifacts/bulk_delete_by_project_service_spec.rb b/spec/services/ci/job_artifacts/bulk_delete_by_project_service_spec.rb
new file mode 100644
index 00000000000..a180837f9a9
--- /dev/null
+++ b/spec/services/ci/job_artifacts/bulk_delete_by_project_service_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::JobArtifacts::BulkDeleteByProjectService, "#execute", feature_category: :build_artifacts do
+ subject(:execute) do
+ described_class.new(
+ job_artifact_ids: job_artifact_ids,
+ current_user: current_user,
+ project: project).execute
+ end
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:build, reload: true) do
+ create(:ci_build, :artifacts, :trace_artifact, user: current_user)
+ end
+
+ let_it_be(:project) { build.project }
+ let_it_be(:job_artifact_ids) { build.job_artifacts.map(&:id) }
+
+ describe '#execute' do
+ context 'when number of artifacts exceeds limits to delete' do
+ let_it_be(:second_build, reload: true) do
+ create(:ci_build, :artifacts, :trace_artifact, user: current_user, project: project)
+ end
+
+ let_it_be(:job_artifact_ids) { ::Ci::JobArtifact.all.map(&:id) }
+
+ before do
+ project.add_maintainer(current_user)
+ stub_const("#{described_class}::JOB_ARTIFACTS_COUNT_LIMIT", 1)
+ end
+
+ it 'fails to destroy' do
+ result = execute
+
+ expect(result).to be_error
+ expect(result[:message]).to eq('Can only delete up to 1 job artifacts per call')
+ end
+ end
+
+ context 'when requested not existing artifacts do delete' do
+ let_it_be(:deleted_build, reload: true) do
+ create(:ci_build, :artifacts, :trace_artifact, user: current_user, project: project)
+ end
+
+ let_it_be(:deleted_job_artifacts) { deleted_build.job_artifacts }
+ let_it_be(:job_artifact_ids) { ::Ci::JobArtifact.all.map(&:id) }
+
+ before do
+ project.add_maintainer(current_user)
+ deleted_job_artifacts.each(&:destroy!)
+ end
+
+ it 'fails to destroy' do
+ result = execute
+
+ expect(result).to be_error
+ expect(result[:message]).to eq("Artifacts (#{deleted_job_artifacts.map(&:id).join(',')}) not found")
+ end
+ end
+
+ context 'when maintainer has access to the project' do
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'is successful' do
+ result = execute
+
+ expect(result).to be_success
+ expect(result.payload).to eq(
+ {
+ destroyed_count: job_artifact_ids.count,
+ destroyed_ids: job_artifact_ids,
+ errors: []
+ }
+ )
+ expect(::Ci::JobArtifact.where(id: job_artifact_ids).count).to eq(0)
+ end
+
+ context 'and partially owns artifacts' do
+ let_it_be(:orphan_artifact) { create(:ci_job_artifact, :archive) }
+ let_it_be(:orphan_artifact_id) { orphan_artifact.id }
+ let_it_be(:owned_artifacts_ids) { build.job_artifacts.erasable.map(&:id) }
+ let_it_be(:job_artifact_ids) { [orphan_artifact_id] + owned_artifacts_ids }
+
+ it 'fails to destroy' do
+ result = execute
+
+ expect(result).to be_error
+ expect(result[:message]).to be('Not all artifacts belong to requested project')
+ expect(::Ci::JobArtifact.where(id: job_artifact_ids).count).to eq(3)
+ end
+ end
+
+ context 'and request all artifacts from a different project' do
+ let_it_be(:different_project_artifact) { create(:ci_job_artifact, :archive) }
+ let_it_be(:job_artifact_ids) { [different_project_artifact] }
+
+ let_it_be(:different_build, reload: true) do
+ create(:ci_build, :artifacts, :trace_artifact, user: current_user)
+ end
+
+ let_it_be(:different_project) { different_build.project }
+
+ before do
+ different_project.add_maintainer(current_user)
+ end
+
+ it 'returns a error' do
+ result = execute
+
+ expect(result).to be_error
+ expect(result[:message]).to be('Not all artifacts belong to requested project')
+ expect(::Ci::JobArtifact.where(id: job_artifact_ids).count).to eq(job_artifact_ids.count)
+ end
+ end
+ end
+ 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 47e9e5994ef..69f760e28ca 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::CreateService do
+RSpec.describe Ci::JobArtifacts::CreateService, feature_category: :build_artifacts do
let_it_be(:project) { create(:project) }
let(:service) { described_class.new(job) }
@@ -33,6 +33,66 @@ RSpec.describe Ci::JobArtifacts::CreateService do
describe '#execute' do
subject { service.execute(artifacts_file, params, metadata_file: metadata_file) }
+ def expect_accessibility_be(accessibility)
+ if accessibility == :public
+ expect(job.job_artifacts).to all be_public_accessibility
+ else
+ expect(job.job_artifacts).to all be_private_accessibility
+ end
+ end
+
+ shared_examples 'job does not have public artifacts in the CI config' do |expected_artifacts_count, accessibility|
+ it "sets accessibility by default to #{accessibility}" do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(expected_artifacts_count)
+
+ expect_accessibility_be(accessibility)
+ end
+ end
+
+ shared_examples 'job artifact set as private in the CI config' do |expected_artifacts_count, accessibility|
+ let!(:job) { create(:ci_build, :with_private_artifacts_config, project: project) }
+
+ it "sets accessibility to #{accessibility}" do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(expected_artifacts_count)
+
+ expect_accessibility_be(accessibility)
+ end
+ end
+
+ shared_examples 'job artifact set as public in the CI config' do |expected_artifacts_count, accessibility|
+ let!(:job) { create(:ci_build, :with_public_artifacts_config, project: project) }
+
+ it "sets accessibility to #{accessibility}" do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(expected_artifacts_count)
+
+ expect_accessibility_be(accessibility)
+ end
+ end
+
+ shared_examples 'when accessibility level passed as private' do |expected_artifacts_count, accessibility|
+ before do
+ params.merge!('accessibility' => 'private')
+ end
+
+ it 'sets accessibility to private level' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(expected_artifacts_count)
+
+ expect_accessibility_be(accessibility)
+ end
+ end
+
+ shared_examples 'when accessibility passed as public' do |expected_artifacts_count|
+ before do
+ params.merge!('accessibility' => 'public')
+ end
+
+ it 'sets accessibility level to public' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(expected_artifacts_count)
+
+ expect(job.job_artifacts).to all be_public_accessibility
+ end
+ end
+
context 'when artifacts file is uploaded' do
it 'logs the created artifact' do
expect(Gitlab::Ci::Artifacts::Logger)
@@ -61,37 +121,19 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect(new_artifact.locked).to eq(job.pipeline.locked)
end
- it 'sets accessibility level by default to public' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- new_artifact = job.job_artifacts.last
- expect(new_artifact).to be_public_accessibility
- end
-
- context 'when accessibility level passed as private' do
+ context 'when non_public_artifacts feature flag is disabled' do
before do
- params.merge!('accessibility' => 'private')
+ stub_feature_flags(non_public_artifacts: false)
end
- it 'sets accessibility level to private' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- new_artifact = job.job_artifacts.last
- expect(new_artifact).to be_private_accessibility
+ context 'when accessibility level not passed to the service' do
+ it_behaves_like 'job does not have public artifacts in the CI config', 1, :public
+ it_behaves_like 'job artifact set as private in the CI config', 1, :public
+ it_behaves_like 'job artifact set as public in the CI config', 1, :public
end
- end
- context 'when accessibility passed as public' do
- before do
- params.merge!('accessibility' => 'public')
- end
-
- it 'sets accessibility to public level' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- new_artifact = job.job_artifacts.last
- expect(new_artifact).to be_public_accessibility
- end
+ it_behaves_like 'when accessibility level passed as private', 1, :public
+ it_behaves_like 'when accessibility passed as public', 1
end
context 'when accessibility passed as invalid value' do
@@ -104,6 +146,16 @@ RSpec.describe Ci::JobArtifacts::CreateService do
end
end
+ context 'when accessibility level not passed to the service' do
+ it_behaves_like 'job does not have public artifacts in the CI config', 1, :public
+ it_behaves_like 'job artifact set as private in the CI config', 1, :private
+ it_behaves_like 'job artifact set as public in the CI config', 1, :public
+ end
+
+ it_behaves_like 'when accessibility level passed as private', 1, :private
+
+ it_behaves_like 'when accessibility passed as public', 1
+
context 'when metadata file is also uploaded' do
let(:metadata_file) do
file_to_upload('spec/fixtures/ci_build_artifacts_metadata.gz', sha256: artifacts_sha256)
@@ -125,13 +177,16 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect(new_artifact.locked).to eq(job.pipeline.locked)
end
- it 'sets accessibility by default to public' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(2)
-
- new_artifact = job.job_artifacts.last
- expect(new_artifact).to be_public_accessibility
+ context 'when accessibility level not passed to the service' do
+ it_behaves_like 'job does not have public artifacts in the CI config', 2, :public
+ it_behaves_like 'job artifact set as private in the CI config', 2, :private
+ it_behaves_like 'job artifact set as public in the CI config', 2, :public
end
+ it_behaves_like 'when accessibility level passed as private', 2, :privatge
+
+ it_behaves_like 'when accessibility passed as public', 2
+
it 'logs the created artifact and metadata' do
expect(Gitlab::Ci::Artifacts::Logger)
.to receive(:log_created)
@@ -140,32 +195,6 @@ RSpec.describe Ci::JobArtifacts::CreateService do
subject
end
- context 'when accessibility level passed as private' do
- before do
- params.merge!('accessibility' => 'private')
- end
-
- it 'sets accessibility to private level' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(2)
-
- new_artifact = job.job_artifacts.last
- expect(new_artifact).to be_private_accessibility
- end
- end
-
- context 'when accessibility passed as public' do
- before do
- params.merge!('accessibility' => 'public')
- end
-
- it 'sets accessibility level to public' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(2)
-
- new_artifact = job.job_artifacts.last
- expect(new_artifact).to be_public_accessibility
- end
- end
-
it 'sets expiration date according to application settings' do
expected_expire_at = 1.day.from_now
diff --git a/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb b/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb
index 74fa42962f3..9c711e54b00 100644
--- a/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb
+++ b/spec/services/ci/job_artifacts/delete_project_artifacts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::DeleteProjectArtifactsService do
+RSpec.describe Ci::JobArtifacts::DeleteProjectArtifactsService, feature_category: :build_artifacts do
let_it_be(:project) { create(:project) }
subject { described_class.new(project: project) }
diff --git a/spec/services/ci/job_artifacts/delete_service_spec.rb b/spec/services/ci/job_artifacts/delete_service_spec.rb
index 78e8be48255..1560d0fc6f4 100644
--- a/spec/services/ci/job_artifacts/delete_service_spec.rb
+++ b/spec/services/ci/job_artifacts/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::DeleteService do
+RSpec.describe Ci::JobArtifacts::DeleteService, feature_category: :build_artifacts do
let_it_be(:build, reload: true) do
create(:ci_build, :artifacts, :trace_artifact, artifacts_expire_at: 100.days.from_now)
end
diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
index 457be67c1ea..d1ec2a1d3a6 100644
--- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
@@ -39,32 +39,12 @@ feature_category: :build_artifacts do
second_artifact
end
- context 'with ci_destroy_unlocked_job_artifacts feature flag disabled' do
- before do
- stub_feature_flags(ci_destroy_unlocked_job_artifacts: false)
- end
-
- it 'performs a consistent number of queries' do
- control = ActiveRecord::QueryRecorder.new { service.execute }
-
- more_artifacts
-
- expect { subject }.not_to exceed_query_limit(control.count)
- end
- end
-
- context 'with ci_destroy_unlocked_job_artifacts feature flag enabled' do
- before do
- stub_feature_flags(ci_destroy_unlocked_job_artifacts: true)
- end
-
- it 'performs a consistent number of queries' do
- control = ActiveRecord::QueryRecorder.new { service.execute }
+ it 'performs a consistent number of queries' do
+ control = ActiveRecord::QueryRecorder.new { service.execute }
- more_artifacts
+ more_artifacts
- expect { subject }.not_to exceed_query_limit(control.count)
- end
+ expect { subject }.not_to exceed_query_limit(control.count)
end
end
@@ -251,6 +231,16 @@ feature_category: :build_artifacts do
end
end
+ context 'when some artifacts are trace' do
+ let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
+ let!(:trace_artifact) { create(:ci_job_artifact, :trace, :expired, job: job, locked: job.pipeline.locked) }
+
+ it 'destroys only non trace artifacts' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
+ expect(trace_artifact).to be_persisted
+ end
+ end
+
context 'when all artifacts are locked' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb
index ca36c923dcf..f4839ccb04b 100644
--- a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb
@@ -2,24 +2,37 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do
+RSpec.describe Ci::JobArtifacts::DestroyAssociationsService, feature_category: :build_artifacts do
let_it_be(:project_1) { create(:project) }
let_it_be(:project_2) { create(:project) }
let_it_be(:artifact_1, refind: true) { create(:ci_job_artifact, :zip, project: project_1) }
- let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :zip, project: project_2) }
- let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :zip, project: project_1) }
+ let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :junit, project: project_2) }
+ let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :terraform, project: project_1) }
+ let_it_be(:artifact_4, refind: true) { create(:ci_job_artifact, :trace, project: project_2) }
+ let_it_be(:artifact_5, refind: true) { create(:ci_job_artifact, :metadata, project: project_2) }
- let(:artifacts) { Ci::JobArtifact.where(id: [artifact_1.id, artifact_2.id, artifact_3.id]) }
+ let_it_be(:locked_artifact, refind: true) { create(:ci_job_artifact, :zip, :locked, project: project_1) }
+
+ let(:artifact_ids_to_be_removed) { [artifact_1.id, artifact_2.id, artifact_3.id, artifact_4.id, artifact_5.id] }
+ let(:artifacts) { Ci::JobArtifact.where(id: artifact_ids_to_be_removed) }
let(:service) { described_class.new(artifacts) }
describe '#destroy_records' do
- it 'removes artifacts without updating statistics' do
+ it 'removes all types of artifacts without updating statistics' do
expect_next_instance_of(Ci::JobArtifacts::DestroyBatchService) do |service|
expect(service).to receive(:execute).with(update_stats: false).and_call_original
end
- expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-3)
+ expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count)
+ end
+
+ context 'with a locked artifact' do
+ let(:artifact_ids_to_be_removed) { [artifact_1.id, locked_artifact.id] }
+
+ it 'removes all artifacts' do
+ expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count)
+ end
end
context 'when there are no artifacts' do
@@ -42,7 +55,11 @@ RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do
have_attributes(amount: -artifact_1.size, ref: artifact_1.id),
have_attributes(amount: -artifact_3.size, ref: artifact_3.id)
]
- project2_increments = [have_attributes(amount: -artifact_2.size, ref: artifact_2.id)]
+ project2_increments = [
+ have_attributes(amount: -artifact_2.size, ref: artifact_2.id),
+ have_attributes(amount: -artifact_4.size, ref: artifact_4.id),
+ have_attributes(amount: -artifact_5.size, ref: artifact_5.id)
+ ]
expect(ProjectStatistics).to receive(:bulk_increment_statistic).once
.with(project_1, :build_artifacts_size, match_array(project1_increments))
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 cde42783d8c..6f9dcf47535 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::DestroyBatchService do
- let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id, trace_artifact.id]) }
+RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_artifacts do
+ let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id]) }
let(:skip_projects_on_refresh) { false }
let(:service) do
described_class.new(
@@ -25,10 +25,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
create(:ci_job_artifact)
end
- let_it_be(:trace_artifact, refind: true) do
- create(:ci_job_artifact, :trace, :expired)
- end
-
describe '#execute' do
subject(:execute) { service.execute }
@@ -60,11 +56,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
execute
end
- it 'preserves trace artifacts' do
- expect { subject }
- .to not_change { Ci::JobArtifact.exists?(trace_artifact.id) }
- end
-
context 'when artifact belongs to a project that is undergoing stats refresh' do
let!(:artifact_under_refresh_1) do
create(:ci_job_artifact, :zip)
@@ -287,7 +278,7 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
end
it 'reports the number of destroyed artifacts' do
- is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success)
+ is_expected.to eq(destroyed_artifacts_count: 0, destroyed_ids: [], statistics_updates: {}, status: :success)
end
end
end
diff --git a/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb b/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb
index fb9dd6b876b..69cdf39107a 100644
--- a/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb
+++ b/spec/services/ci/job_artifacts/expire_project_build_artifacts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsService do
+RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsService, feature_category: :build_artifacts do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline, reload: true) { create(:ci_pipeline, :unlocked, project: project) }
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
index d4d56825e1f..5355bec2d6c 100644
--- a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
+++ b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
+RSpec.describe Ci::JobArtifacts::TrackArtifactReportService, feature_category: :build_artifacts do
describe '#execute', :clean_gitlab_redis_shared_state do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb b/spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb
index 67412e41fb8..5f6a89b89e1 100644
--- a/spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb
+++ b/spec/services/ci/job_artifacts/update_unknown_locked_status_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::UpdateUnknownLockedStatusService, :clean_gitlab_redis_shared_state do
+RSpec.describe Ci::JobArtifacts::UpdateUnknownLockedStatusService, :clean_gitlab_redis_shared_state,
+ feature_category: :build_artifacts do
include ExclusiveLeaseHelpers
let(:service) { described_class.new }
diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index e2bbdefef7f..56a392221be 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::ListConfigVariablesService,
-:use_clean_rails_memory_store_caching, feature_category: :pipeline_authoring do
+:use_clean_rails_memory_store_caching, feature_category: :pipeline_composition do
include ReactiveCachingHelpers
let(:ci_config) { {} }
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 c4558bddc85..b7b32d2a0af 100644
--- a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
+++ b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineArtifacts::CoverageReportService do
+RSpec.describe Ci::PipelineArtifacts::CoverageReportService, feature_category: :build_artifacts do
describe '#execute' do
let_it_be(:project) { create(:project, :repository) }
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 5d854b61f14..20265a0ca48 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService do
+RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService, feature_category: :build_artifacts do
describe '#execute' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb
index 47e8766c215..b46648760e1 100644
--- a/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state do
+RSpec.describe Ci::PipelineArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state,
+ feature_category: :build_artifacts do
let(:service) { described_class.new }
describe '.execute' do
diff --git a/spec/services/ci/pipeline_bridge_status_service_spec.rb b/spec/services/ci/pipeline_bridge_status_service_spec.rb
index 1346f68c952..3d8219251d6 100644
--- a/spec/services/ci/pipeline_bridge_status_service_spec.rb
+++ b/spec/services/ci/pipeline_bridge_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineBridgeStatusService do
+RSpec.describe Ci::PipelineBridgeStatusService, feature_category: :continuous_integration do
let(:user) { build(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb b/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb
index ab4ba20e716..06139c091b9 100644
--- a/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb
+++ b/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineCreation::StartPipelineService do
+RSpec.describe Ci::PipelineCreation::StartPipelineService, feature_category: :continuous_integration do
let(:pipeline) { build(:ci_pipeline) }
subject(:service) { described_class.new(pipeline) }
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 d0aa1ba4c6c..46ea0036e49 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
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection do
+RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection,
+ feature_category: :continuous_integration do
using RSpec::Parameterized::TableSyntax
let_it_be(:pipeline) { create(:ci_pipeline) }
@@ -35,7 +36,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
it 'does update existing status of processable' do
collection.set_processable_status(test_a.id, 'success', 100)
- expect(collection.status_for_names(['test-a'], dag: false)).to eq('success')
+ expect(collection.status_of_processables(['test-a'], dag: false)).to eq('success')
end
it 'ignores a missing processable' do
@@ -49,7 +50,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
end
end
- describe '#status_for_names' do
+ describe '#status_of_processables' do
where(:names, :status, :dag) do
%w[build-a] | 'success' | false
%w[build-a build-b] | 'failed' | false
@@ -61,12 +62,12 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
with_them do
it 'returns composite status of given names' do
- expect(collection.status_for_names(names, dag: dag)).to eq(status)
+ expect(collection.status_of_processables(names, dag: dag)).to eq(status)
end
end
end
- describe '#status_for_prior_stage_position' do
+ describe '#status_of_processables_prior_to_stage' do
where(:stage, :status) do
0 | 'success'
1 | 'failed'
@@ -75,12 +76,12 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
with_them do
it 'returns composite status for processables in prior stages' do
- expect(collection.status_for_prior_stage_position(stage)).to eq(status)
+ expect(collection.status_of_processables_prior_to_stage(stage)).to eq(status)
end
end
end
- describe '#status_for_stage_position' do
+ describe '#status_of_stage' do
where(:stage, :status) do
0 | 'failed'
1 | 'running'
@@ -89,16 +90,16 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
with_them do
it 'returns composite status for processables at a given stages' do
- expect(collection.status_for_stage_position(stage)).to eq(status)
+ expect(collection.status_of_stage(stage)).to eq(status)
end
end
end
- describe '#created_processable_ids_for_stage_position' do
+ describe '#created_processable_ids_in_stage' do
it 'returns IDs of processables at a given stage position' do
- expect(collection.created_processable_ids_for_stage_position(0)).to be_empty
- expect(collection.created_processable_ids_for_stage_position(1)).to be_empty
- expect(collection.created_processable_ids_for_stage_position(2)).to contain_exactly(deploy.id)
+ expect(collection.created_processable_ids_in_stage(0)).to be_empty
+ expect(collection.created_processable_ids_in_stage(1)).to be_empty
+ expect(collection.created_processable_ids_in_stage(2)).to contain_exactly(deploy.id)
end
end
diff --git a/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb b/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
index 9a3aad20d89..1d45a06f9ea 100644
--- a/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
+++ b/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineSchedules::TakeOwnershipService do
+RSpec.describe Ci::PipelineSchedules::TakeOwnershipService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:reporter) { create(:user) }
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index 4946367380e..b6e07e82bb5 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineTriggerService do
+RSpec.describe Ci::PipelineTriggerService, feature_category: :continuous_integration do
include AfterNextHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/ci/pipelines/add_job_service_spec.rb b/spec/services/ci/pipelines/add_job_service_spec.rb
index c62aa9506bd..9fb1d6933c6 100644
--- a/spec/services/ci/pipelines/add_job_service_spec.rb
+++ b/spec/services/ci/pipelines/add_job_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Pipelines::AddJobService do
+RSpec.describe Ci::Pipelines::AddJobService, feature_category: :continuous_integration do
include ExclusiveLeaseHelpers
let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }
diff --git a/spec/services/ci/pipelines/hook_service_spec.rb b/spec/services/ci/pipelines/hook_service_spec.rb
index 8d138a3d957..e773ae2d2c3 100644
--- a/spec/services/ci/pipelines/hook_service_spec.rb
+++ b/spec/services/ci/pipelines/hook_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Pipelines::HookService do
+RSpec.describe Ci::Pipelines::HookService, feature_category: :continuous_integration do
describe '#execute_hooks' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
diff --git a/spec/services/ci/play_bridge_service_spec.rb b/spec/services/ci/play_bridge_service_spec.rb
index 56b1615a56d..5727ed64f8b 100644
--- a/spec/services/ci/play_bridge_service_spec.rb
+++ b/spec/services/ci/play_bridge_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PlayBridgeService, '#execute' do
+RSpec.describe Ci::PlayBridgeService, '#execute', feature_category: :continuous_integration do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index fc07801b672..f439538b4cc 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PlayBuildService, '#execute' do
+RSpec.describe Ci::PlayBuildService, '#execute', feature_category: :continuous_integration do
let(:user) { create(:user, developer_projects: [project]) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/play_manual_stage_service_spec.rb b/spec/services/ci/play_manual_stage_service_spec.rb
index 24f0a21f3dd..1aca4ffec2d 100644
--- a/spec/services/ci/play_manual_stage_service_spec.rb
+++ b/spec/services/ci/play_manual_stage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PlayManualStageService, '#execute' do
+RSpec.describe Ci::PlayManualStageService, '#execute', feature_category: :continuous_integration do
let(:current_user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, user: current_user) }
let(:project) { pipeline.project }
diff --git a/spec/services/ci/prepare_build_service_spec.rb b/spec/services/ci/prepare_build_service_spec.rb
index f75cb322fe9..8583b8e667c 100644
--- a/spec/services/ci/prepare_build_service_spec.rb
+++ b/spec/services/ci/prepare_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PrepareBuildService do
+RSpec.describe Ci::PrepareBuildService, feature_category: :continuous_integration do
describe '#execute' do
let(:build) { create(:ci_build, :preparing) }
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index de308bb1a87..d1442b75731 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::ProcessBuildService, '#execute' do
+RSpec.describe Ci::ProcessBuildService, '#execute', feature_category: :continuous_integration do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 404e1bf7c87..d1586ad4c8b 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ProcessPipelineService do
+RSpec.describe Ci::ProcessPipelineService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let(:pipeline) do
diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb
index 7ab7911e578..c042a9bd46e 100644
--- a/spec/services/ci/process_sync_events_service_spec.rb
+++ b/spec/services/ci/process_sync_events_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ProcessSyncEventsService do
+RSpec.describe Ci::ProcessSyncEventsService, feature_category: :continuous_integration do
let!(:group) { create(:group) }
let!(:project1) { create(:project, group: group) }
let!(:project2) { create(:project, group: group) }
diff --git a/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb b/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb
index 0b100af5902..a9ee5216d81 100644
--- a/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb
+++ b/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PrometheusMetrics::ObserveHistogramsService do
+RSpec.describe Ci::PrometheusMetrics::ObserveHistogramsService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let(:params) { {} }
diff --git a/spec/services/ci/queue/pending_builds_strategy_spec.rb b/spec/services/ci/queue/pending_builds_strategy_spec.rb
index 6f22c256c17..ea9207ddb8f 100644
--- a/spec/services/ci/queue/pending_builds_strategy_spec.rb
+++ b/spec/services/ci/queue/pending_builds_strategy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Queue::PendingBuildsStrategy do
+RSpec.describe Ci::Queue::PendingBuildsStrategy, feature_category: :continuous_integration 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) }
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 9183df359b4..18cb016f94a 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -16,126 +16,155 @@ module Ci
describe '#execute' do
subject(:execute) { described_class.new(runner, runner_machine).execute }
- context 'with runner_machine specified' do
- let(:runner) { project_runner }
- let!(:runner_machine) { create(:ci_runner_machine, runner: project_runner) }
+ let(:runner_machine) { nil }
+
+ context 'checks database loadbalancing stickiness' do
+ let(:runner) { shared_runner }
before do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- project_runner.update!(tag_list: ["linux"])
+ project.update!(shared_runners_enabled: false)
end
- it 'sets runner_machine on job' do
- expect { execute }.to change { pending_job.reload.runner_machine }.from(nil).to(runner_machine)
+ it 'result is valid if replica did caught-up', :aggregate_failures do
+ expect(ApplicationRecord.sticking).to receive(:all_caught_up?).with(:runner, runner.id) { true }
- expect(execute.build).to eq(pending_job)
+ expect { execute }.not_to change { Ci::RunnerMachineBuild.count }.from(0)
+ expect(execute).to be_valid
+ expect(execute.build).to be_nil
+ expect(execute.build_json).to be_nil
end
- end
-
- context 'with no runner machine' do
- let(:runner_machine) { nil }
-
- context 'checks database loadbalancing stickiness' do
- let(:runner) { shared_runner }
-
- before do
- project.update!(shared_runners_enabled: false)
- end
- it 'result is valid if replica did caught-up', :aggregate_failures do
- expect(ApplicationRecord.sticking).to receive(:all_caught_up?).with(:runner, runner.id) { true }
+ it 'result is invalid if replica did not caught-up', :aggregate_failures do
+ expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
+ .with(:runner, shared_runner.id) { false }
- expect(execute).to be_valid
- expect(execute.build).to be_nil
- expect(execute.build_json).to be_nil
- end
+ expect(subject).not_to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
+ end
+ end
- it 'result is invalid if replica did not caught-up', :aggregate_failures do
- expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
- .with(:runner, shared_runner.id) { false }
+ shared_examples 'handles runner assignment' do
+ context 'runner follows tag list' do
+ subject(:build) { build_on(project_runner, runner_machine: project_runner_machine) }
- expect(subject).not_to be_valid
- expect(subject.build).to be_nil
- expect(subject.build_json).to be_nil
- end
- end
+ let(:project_runner_machine) { nil }
- shared_examples 'handles runner assignment' do
- context 'runner follow tag list' do
- it "picks build with the same tag" do
+ context 'when job has tag' do
+ before do
pending_job.update!(tag_list: ["linux"])
pending_job.reload
pending_job.create_queuing_entry!
- project_runner.update!(tag_list: ["linux"])
- expect(build_on(project_runner)).to eq(pending_job)
end
- it "does not pick build with different tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- project_runner.update!(tag_list: ["win32"])
- expect(build_on(project_runner)).to be_falsey
+ context 'and runner has matching tag' do
+ before do
+ project_runner.update!(tag_list: ["linux"])
+ end
+
+ context 'with no runner machine specified' do
+ it 'picks build' do
+ expect(build).to eq(pending_job)
+ expect(pending_job.runner_machine).to be_nil
+ end
+ end
+
+ context 'with runner machine specified' do
+ let(:project_runner_machine) { create(:ci_runner_machine, runner: project_runner) }
+
+ it 'picks build and assigns runner machine' do
+ expect(build).to eq(pending_job)
+ expect(pending_job.runner_machine).to eq(project_runner_machine)
+ end
+ end
end
- it "picks build without tag" do
- expect(build_on(project_runner)).to eq(pending_job)
+ it 'does not pick build with different tag' do
+ project_runner.update!(tag_list: ["win32"])
+ expect(build).to be_falsey
end
- it "does not pick build with tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
+ it 'does not pick build with tag' do
pending_job.create_queuing_entry!
- expect(build_on(project_runner)).to be_falsey
+ expect(build).to be_falsey
end
+ end
- it "pick build without tag" do
- project_runner.update!(tag_list: ["win32"])
- expect(build_on(project_runner)).to eq(pending_job)
+ context 'when job has no tag' do
+ it 'picks build' do
+ expect(build).to eq(pending_job)
+ end
+
+ context 'when runner has tag' do
+ before do
+ project_runner.update!(tag_list: ["win32"])
+ end
+
+ it 'picks build' do
+ expect(build).to eq(pending_job)
+ end
end
end
+ end
- context 'deleted projects' do
+ context 'deleted projects' do
+ before do
+ project.update!(pending_delete: true)
+ end
+
+ context 'for shared runners' do
before do
- project.update!(pending_delete: true)
+ project.update!(shared_runners_enabled: true)
end
- context 'for shared runners' do
- before do
- project.update!(shared_runners_enabled: true)
- end
+ it 'does not pick a build' do
+ expect(build_on(shared_runner)).to be_nil
+ end
+ end
+
+ context 'for project runner' do
+ subject(:build) { build_on(project_runner, runner_machine: project_runner_machine) }
+ let(:project_runner_machine) { nil }
+
+ context 'with no runner machine specified' do
it 'does not pick a build' do
- expect(build_on(shared_runner)).to be_nil
+ expect(build).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job.queuing_entry).to be_nil
+ expect(Ci::RunnerMachineBuild.all).to be_empty
end
end
- context 'for project runner' do
+ context 'with runner machine specified' do
+ let(:project_runner_machine) { create(:ci_runner_machine, runner: project_runner) }
+
it 'does not pick a build' do
- expect(build_on(project_runner)).to be_nil
+ expect(build).to be_nil
expect(pending_job.reload).to be_failed
expect(pending_job.queuing_entry).to be_nil
+ expect(Ci::RunnerMachineBuild.all).to be_empty
end
end
end
+ end
- context 'allow shared runners' do
- before do
- project.update!(shared_runners_enabled: true)
- pipeline.reload
- pending_job.reload
- pending_job.create_queuing_entry!
- end
+ context 'allow shared runners' do
+ before do
+ project.update!(shared_runners_enabled: true)
+ pipeline.reload
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ end
- context 'when build owner has been blocked' do
- let(:user) { create(:user, :blocked) }
+ context 'when build owner has been blocked' do
+ let(:user) { create(:user, :blocked) }
- before do
- pending_job.update!(user: user)
- end
+ before do
+ pending_job.update!(user: user)
+ end
+ context 'with no runner machine specified' do
it 'does not pick the build and drops the build' do
expect(build_on(shared_runner)).to be_falsey
@@ -143,690 +172,701 @@ module Ci
end
end
- context 'for multiple builds' do
- let!(:project2) { create :project, shared_runners_enabled: true }
- let!(:pipeline2) { create :ci_pipeline, project: project2 }
- let!(:project3) { create :project, shared_runners_enabled: true }
- let!(:pipeline3) { create :ci_pipeline, project: project3 }
- let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
+ context 'with runner machine specified' do
+ let(:runner_machine) { create(:ci_runner_machine, runner: runner) }
- it 'picks builds one-by-one' do
- expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
+ it 'does not pick the build and does not create join record' do
+ expect(build_on(shared_runner, runner_machine: runner_machine)).to be_falsey
- expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(Ci::RunnerMachineBuild.all).to be_empty
end
+ end
+ end
- context 'when using fair scheduling' do
- context 'when all builds are pending' do
- it 'prefers projects without builds first' do
- # it gets for one build from each of the projects
- expect(build_on(shared_runner)).to eq(build1_project1)
- expect(build_on(shared_runner)).to eq(build1_project2)
- expect(build_on(shared_runner)).to eq(build1_project3)
-
- # then it gets a second build from each of the projects
- expect(build_on(shared_runner)).to eq(build2_project1)
- expect(build_on(shared_runner)).to eq(build2_project2)
-
- # in the end the third build
- expect(build_on(shared_runner)).to eq(build3_project1)
- end
- end
+ context 'for multiple builds' do
+ let!(:project2) { create :project, shared_runners_enabled: true }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :project, shared_runners_enabled: true }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
+
+ it 'picks builds one-by-one' do
+ expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
+
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ end
- context 'when some builds transition to success' do
- it 'equalises number of running builds' do
- # after finishing the first build for project 1, get a second build from the same project
- expect(build_on(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(build_on(shared_runner)).to eq(build2_project1)
-
- expect(build_on(shared_runner)).to eq(build1_project2)
- build1_project2.reload.success
- expect(build_on(shared_runner)).to eq(build2_project2)
- expect(build_on(shared_runner)).to eq(build1_project3)
- expect(build_on(shared_runner)).to eq(build3_project1)
- end
+ context 'when using fair scheduling' do
+ context 'when all builds are pending' do
+ it 'prefers projects without builds first' do
+ # it gets for one build from each of the projects
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+
+ # then it gets a second build from each of the projects
+ expect(build_on(shared_runner)).to eq(build2_project1)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+
+ # in the end the third build
+ expect(build_on(shared_runner)).to eq(build3_project1)
end
end
- context 'when using DEFCON mode that disables fair scheduling' do
- before do
- stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
+ context 'when some builds transition to success' do
+ it 'equalises number of running builds' do
+ # after finishing the first build for project 1, get a second build from the same project
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project1)
+
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ build1_project2.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ expect(build_on(shared_runner)).to eq(build3_project1)
end
+ end
+ end
- context 'when all builds are pending' do
- it 'returns builds in order of creation (FIFO)' do
- # it gets for one build from each of the projects
- expect(build_on(shared_runner)).to eq(build1_project1)
- expect(build_on(shared_runner)).to eq(build2_project1)
- expect(build_on(shared_runner)).to eq(build3_project1)
- expect(build_on(shared_runner)).to eq(build1_project2)
- expect(build_on(shared_runner)).to eq(build2_project2)
- expect(build_on(shared_runner)).to eq(build1_project3)
- end
+ context 'when using DEFCON mode that disables fair scheduling' do
+ before do
+ stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
+ end
+
+ context 'when all builds are pending' do
+ it 'returns builds in order of creation (FIFO)' do
+ # it gets for one build from each of the projects
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(build_on(shared_runner)).to eq(build2_project1)
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
end
+ end
- context 'when some builds transition to success' do
- it 'returns builds in order of creation (FIFO)' do
- expect(build_on(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(build_on(shared_runner)).to eq(build2_project1)
-
- expect(build_on(shared_runner)).to eq(build3_project1)
- build2_project1.reload.success
- expect(build_on(shared_runner)).to eq(build1_project2)
- expect(build_on(shared_runner)).to eq(build2_project2)
- expect(build_on(shared_runner)).to eq(build1_project3)
- end
+ context 'when some builds transition to success' do
+ it 'returns builds in order of creation (FIFO)' do
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project1)
+
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ build2_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
end
end
end
+ end
- context 'shared runner' do
- let(:response) { described_class.new(shared_runner, nil).execute }
- let(:build) { response.build }
+ context 'shared runner' do
+ let(:response) { described_class.new(shared_runner, nil).execute }
+ let(:build) { response.build }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(shared_runner) }
- it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
- end
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(shared_runner) }
+ it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
+ end
- context 'project runner' do
- let(:build) { build_on(project_runner) }
+ context 'project runner' do
+ let(:build) { build_on(project_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(project_runner) }
- end
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(project_runner) }
end
+ end
- context 'disallow shared runners' do
- before do
- project.update!(shared_runners_enabled: false)
- end
+ context 'disallow shared runners' do
+ before do
+ project.update!(shared_runners_enabled: false)
+ end
- context 'shared runner' do
- let(:build) { build_on(shared_runner) }
+ context 'shared runner' do
+ let(:build) { build_on(shared_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'project runner' do
- let(:build) { build_on(project_runner) }
+ context 'project runner' do
+ let(:build) { build_on(project_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(project_runner) }
- end
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(project_runner) }
end
+ end
- context 'disallow when builds are disabled' do
- before do
- project.update!(shared_runners_enabled: true, group_runners_enabled: true)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ context 'disallow when builds are disabled' do
+ before do
+ project.update!(shared_runners_enabled: true, group_runners_enabled: true)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
- pending_job.reload.create_queuing_entry!
- end
+ pending_job.reload.create_queuing_entry!
+ end
- context 'and uses shared runner' do
- let(:build) { build_on(shared_runner) }
+ context 'and uses shared runner' do
+ let(:build) { build_on(shared_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'and uses group runner' do
- let(:build) { build_on(group_runner) }
+ context 'and uses group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'and uses project runner' do
- let(:build) { build_on(project_runner) }
+ context 'and uses project runner' do
+ let(:build) { build_on(project_runner) }
- it 'does not pick a build' do
- expect(build).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job.queuing_entry).to be_nil
- end
+ it 'does not pick a build' do
+ expect(build).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job.queuing_entry).to be_nil
end
end
+ end
- context 'allow group runners' do
- before do
- project.update!(group_runners_enabled: true)
- end
+ context 'allow group runners' do
+ before do
+ project.update!(group_runners_enabled: true)
+ end
- context 'for multiple builds' do
- let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
- let!(:pipeline2) { create(:ci_pipeline, project: project2) }
- let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
- let!(:pipeline3) { create(:ci_pipeline, project: project3) }
+ context 'for multiple builds' do
+ let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline2) { create(:ci_pipeline, project: project2) }
+ let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline3) { create(:ci_pipeline, project: project3) }
- let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
- # these shouldn't influence the scheduling
- let!(:unrelated_group) { create(:group) }
- let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
- let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
- let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
- let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
+ # these shouldn't influence the scheduling
+ let!(:unrelated_group) { create(:group) }
+ let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
+ let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
+ let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
+ let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
- it 'does not consider builds from other group runners' do
- queue = ::Ci::Queue::BuildQueueService.new(group_runner)
+ it 'does not consider builds from other group runners' do
+ queue = ::Ci::Queue::BuildQueueService.new(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 6
- build_on(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 6
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 5
- build_on(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 5
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 4
- build_on(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 4
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 3
- build_on(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 3
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 2
- build_on(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 2
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 1
- build_on(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 1
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 0
- expect(build_on(group_runner)).to be_nil
- end
+ expect(queue.builds_for_group_runner.size).to eq 0
+ expect(build_on(group_runner)).to be_nil
end
+ end
- context 'group runner' do
- let(:build) { build_on(group_runner) }
+ context 'group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(group_runner) }
- end
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(group_runner) }
end
+ end
- context 'disallow group runners' do
- before do
- project.update!(group_runners_enabled: false)
+ context 'disallow group runners' do
+ before do
+ project.update!(group_runners_enabled: false)
- pending_job.reload.create_queuing_entry!
- end
+ pending_job.reload.create_queuing_entry!
+ end
- context 'group runner' do
- let(:build) { build_on(group_runner) }
+ context 'group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
end
+ end
- context 'when first build is stalled' do
- before do
- allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original
- allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!)
- .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
- end
+ context 'when first build is stalled' do
+ before do
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!)
+ .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
+ end
- subject { described_class.new(project_runner, nil).execute }
+ subject { described_class.new(project_runner, nil).execute }
- context 'with multiple builds are in queue' do
- let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'with multiple builds are in queue' do
+ let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
- end
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
+ end
- it "receives second build from the queue" do
- expect(subject).to be_valid
- expect(subject.build).to eq(other_build)
- end
+ it "receives second build from the queue" do
+ expect(subject).to be_valid
+ expect(subject.build).to eq(other_build)
end
+ end
- context 'when single build is in queue' do
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return(Ci::Build.where(id: pending_job).pluck(:id))
- end
+ context 'when single build is in queue' do
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return(Ci::Build.where(id: pending_job).pluck(:id))
+ end
- it "does not receive any valid result" do
- expect(subject).not_to be_valid
- end
+ it "does not receive any valid result" do
+ expect(subject).not_to be_valid
end
+ end
- context 'when there is no build in queue' do
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return([])
- end
+ context 'when there is no build in queue' do
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return([])
+ end
- it "does not receive builds but result is valid" do
- expect(subject).to be_valid
- expect(subject.build).to be_nil
- end
+ it "does not receive builds but result is valid" do
+ expect(subject).to be_valid
+ expect(subject.build).to be_nil
end
end
+ end
- context 'when access_level of runner is not_protected' do
- let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ context 'when access_level of runner is not_protected' do
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
- context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
- it 'picks the job' do
- expect(build_on(project_runner)).to eq(pending_job)
- end
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
end
+ end
- context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- it 'picks the job' do
- expect(build_on(project_runner)).to eq(pending_job)
- end
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
end
+ end
- context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- pending_job.update_attribute(:protected, nil)
- end
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
- it 'picks the job' do
- expect(build_on(project_runner)).to eq(pending_job)
- end
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
end
end
+ end
- context 'when access_level of runner is ref_protected' do
- let!(:project_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
+ context 'when access_level of runner is ref_protected' do
+ let!(:project_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
- context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
- it 'picks the job' do
- expect(build_on(project_runner)).to eq(pending_job)
- end
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
end
+ end
- context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- it 'does not pick the job' do
- expect(build_on(project_runner)).to be_nil
- end
+ it 'does not pick the job' do
+ expect(build_on(project_runner)).to be_nil
end
+ end
- context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- pending_job.update_attribute(:protected, nil)
- end
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
- it 'does not pick the job' do
- expect(build_on(project_runner)).to be_nil
- end
+ it 'does not pick the job' do
+ expect(build_on(project_runner)).to be_nil
end
end
+ end
- context 'runner feature set is verified' do
- let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
+ context 'runner feature set is verified' do
+ let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
- subject { build_on(project_runner, params: params) }
+ subject { build_on(project_runner, params: params) }
- context 'when feature is missing by runner' do
- let(:params) { {} }
+ context 'when feature is missing by runner' do
+ let(:params) { {} }
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job).to be_runner_unsupported
- end
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_runner_unsupported
end
+ end
- context 'when feature is supported by runner' do
- let(:params) do
- { info: { features: { upload_multiple_artifacts: true } } }
- end
+ context 'when feature is supported by runner' do
+ let(:params) do
+ { info: { features: { upload_multiple_artifacts: true } } }
+ end
- it 'does pick job' do
- expect(subject).not_to be_nil
- end
+ it 'does pick job' do
+ expect(subject).not_to be_nil
end
end
+ end
- context 'when "dependencies" keyword is specified' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
+ context 'when "dependencies" keyword is specified' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- let!(:pending_job) do
- create(:ci_build, :pending, :queued,
- pipeline: pipeline, stage_idx: 1,
- options: { script: ["bash"], dependencies: dependencies })
- end
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline, stage_idx: 1,
+ options: { script: ["bash"], dependencies: dependencies })
+ end
- let(:dependencies) { %w[test] }
+ let(:dependencies) { %w[test] }
- subject { build_on(project_runner) }
+ subject { build_on(project_runner) }
- it 'picks a build with a dependency' do
- picked_build = build_on(project_runner)
+ it 'picks a build with a dependency' do
+ picked_build = build_on(project_runner)
- expect(picked_build).to be_present
+ expect(picked_build).to be_present
+ end
+
+ context 'when there are multiple dependencies with artifacts' do
+ let!(:pre_stage_job_second) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
end
- context 'when there are multiple dependencies with artifacts' do
- let!(:pre_stage_job_second) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
- end
+ let(:dependencies) { %w[test deploy] }
- let(:dependencies) { %w[test deploy] }
+ it 'logs build artifacts size' do
+ build_on(project_runner)
- it 'logs build artifacts size' do
- build_on(project_runner)
+ artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
+ job.job_artifacts_archive.size
+ end
- artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
- job.job_artifacts_archive.size
- end
+ expect(artifacts_size).to eq 107464 * 2
+ expect(Gitlab::ApplicationContext.current).to include({
+ 'meta.artifacts_dependencies_size' => artifacts_size,
+ 'meta.artifacts_dependencies_count' => 2
+ })
+ end
+ end
- expect(artifacts_size).to eq 107464 * 2
- expect(Gitlab::ApplicationContext.current).to include({
- 'meta.artifacts_dependencies_size' => artifacts_size,
- 'meta.artifacts_dependencies_count' => 2
- })
- end
+ shared_examples 'not pick' do
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_missing_dependency_failure
end
+ end
- shared_examples 'not pick' do
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job).to be_missing_dependency_failure
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- end
- shared_examples 'validation is active' do
- context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) do
- create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
+ it { is_expected.to eq(pending_job) }
+ end
- it { is_expected.to eq(pending_job) }
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- context 'when artifacts of depended job has been expired' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
+ context 'when the pipeline is locked' do
+ before do
+ pipeline.artifacts_locked!
end
- context 'when the pipeline is locked' do
- before do
- pipeline.artifacts_locked!
- end
+ it { is_expected.to eq(pending_job) }
+ end
- it { is_expected.to eq(pending_job) }
+ context 'when the pipeline is unlocked' do
+ before do
+ pipeline.unlocked!
end
- context 'when the pipeline is unlocked' do
- before do
- pipeline.unlocked!
- end
+ it_behaves_like 'not pick'
+ end
+ end
- it_behaves_like 'not pick'
- end
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
end
- context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
- end
+ it_behaves_like 'not pick'
+ end
- it_behaves_like 'not pick'
+ context 'when job object is staled' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- context 'when job object is staled' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
-
- before do
- pipeline.unlocked!
+ before do
+ pipeline.unlocked!
- allow_next_instance_of(Ci::Build) do |build|
- expect(build).to receive(:drop!)
- .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
- end
+ allow_next_instance_of(Ci::Build) do |build|
+ expect(build).to receive(:drop!)
+ .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
end
+ end
- it 'does not drop nor pick' do
- expect(subject).to be_nil
- end
+ it 'does not drop nor pick' do
+ expect(subject).to be_nil
end
end
+ end
- shared_examples 'validation is not active' do
- context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) do
- create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
-
- it { expect(subject).to eq(pending_job) }
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- context 'when artifacts of depended job has been expired' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
+ it { expect(subject).to eq(pending_job) }
+ end
- it { expect(subject).to eq(pending_job) }
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
- end
+ it { expect(subject).to eq(pending_job) }
+ end
- it { expect(subject).to eq(pending_job) }
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
end
- end
- it_behaves_like 'validation is active'
+ it { expect(subject).to eq(pending_job) }
+ end
end
- context 'when build is degenerated' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
+ it_behaves_like 'validation is active'
+ end
- subject { build_on(project_runner) }
+ context 'when build is degenerated' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
+ subject { build_on(project_runner) }
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_archived_failure
- end
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_archived_failure
end
+ end
- context 'when build has data integrity problem' do
- let!(:pending_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline)
- end
+ context 'when build has data integrity problem' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
+ end
- before do
- pending_job.update_columns(options: "string")
- end
+ before do
+ pending_job.update_columns(options: "string")
+ end
- subject { build_on(project_runner) }
+ subject { build_on(project_runner) }
- it 'does drop the build and logs both failures' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(anything, a_hash_including(build_id: pending_job.id))
- .twice
- .and_call_original
+ it 'does drop the build and logs both failures' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(anything, a_hash_including(build_id: pending_job.id))
+ .twice
+ .and_call_original
- expect(subject).to be_nil
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_data_integrity_failure
- end
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_data_integrity_failure
end
+ end
- context 'when build fails to be run!' do
- let!(:pending_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline)
- end
+ context 'when build fails to be run!' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
+ end
- before do
- expect_any_instance_of(Ci::Build).to receive(:run!)
- .and_raise(RuntimeError, 'scheduler error')
- end
+ before do
+ expect_any_instance_of(Ci::Build).to receive(:run!)
+ .and_raise(RuntimeError, 'scheduler error')
+ end
- subject { build_on(project_runner) }
+ subject { build_on(project_runner) }
- it 'does drop the build and logs failure' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(anything, a_hash_including(build_id: pending_job.id))
- .once
- .and_call_original
+ it 'does drop the build and logs failure' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(anything, a_hash_including(build_id: pending_job.id))
+ .once
+ .and_call_original
- expect(subject).to be_nil
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_scheduler_failure
- end
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_scheduler_failure
end
+ end
- context 'when an exception is raised during a persistent ref creation' do
- before do
- allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
- allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
- end
+ context 'when an exception is raised during a persistent ref creation' do
+ before do
+ allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
+ allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
+ end
- subject { build_on(project_runner) }
+ subject { build_on(project_runner) }
- it 'picks the build' do
- expect(subject).to eq(pending_job)
+ it 'picks the build' do
+ expect(subject).to eq(pending_job)
- pending_job.reload
- expect(pending_job).to be_running
- end
+ pending_job.reload
+ expect(pending_job).to be_running
end
+ end
- context 'when only some builds can be matched by runner' do
- let!(:project_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
+ context 'when only some builds can be matched by runner' do
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
- before do
- # create additional matching and non-matching jobs
- create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
- create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
- end
+ before do
+ # create additional matching and non-matching jobs
+ create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
+ create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
+ end
- it 'observes queue size of only matching jobs' do
- # pending_job + 2 x matching ones
- expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
- .with({ runner_type: project_runner.runner_type }, 3)
+ it 'observes queue size of only matching jobs' do
+ # pending_job + 2 x matching ones
+ expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, 3)
- expect(build_on(project_runner)).to eq(pending_job)
- end
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
- it 'observes queue processing time by the runner type' do
- expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
- .to receive(:observe)
- .with({ runner_type: project_runner.runner_type }, anything)
+ it 'observes queue processing time by the runner type' do
+ expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
+ .to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, anything)
- expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
- .to receive(:observe)
- .with({ runner_type: project_runner.runner_type }, anything)
+ expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
+ .to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, anything)
- expect(build_on(project_runner)).to eq(pending_job)
- end
+ expect(build_on(project_runner)).to eq(pending_job)
end
+ end
- context 'when ci_register_job_temporary_lock is enabled' do
- before do
- stub_feature_flags(ci_register_job_temporary_lock: true)
+ context 'when ci_register_job_temporary_lock is enabled' do
+ before do
+ stub_feature_flags(ci_register_job_temporary_lock: true)
+
+ allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ end
- allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ context 'when a build is temporarily locked' do
+ let(:service) { described_class.new(project_runner, nil) }
+
+ before do
+ service.send(:acquire_temporary_lock, pending_job.id)
end
- context 'when a build is temporarily locked' do
- let(:service) { described_class.new(project_runner, nil) }
+ it 'skips this build and marks queue as invalid' do
+ expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ .with(operation: :queue_iteration)
+ expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ .with(operation: :build_temporary_locked)
- before do
- service.send(:acquire_temporary_lock, pending_job.id)
- end
+ expect(service.execute).not_to be_valid
+ end
- it 'skips this build and marks queue as invalid' do
+ context 'when there is another build in queue' do
+ let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+
+ it 'skips this build and picks another build' do
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :queue_iteration)
+ .with(operation: :queue_iteration).twice
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
.with(operation: :build_temporary_locked)
- expect(service.execute).not_to be_valid
- end
-
- context 'when there is another build in queue' do
- let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
-
- it 'skips this build and picks another build' do
- expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :queue_iteration).twice
- expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :build_temporary_locked)
+ result = service.execute
- result = service.execute
-
- expect(result.build).to eq(next_pending_job)
- expect(result).to be_valid
- end
+ expect(result.build).to eq(next_pending_job)
+ expect(result).to be_valid
end
end
end
end
+ end
- context 'when using pending builds table' do
- include_examples 'handles runner assignment'
+ context 'when using pending builds table' do
+ let!(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }
- context 'when a conflicting data is stored in denormalized table' do
- let!(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }
+ include_examples 'handles runner assignment'
- before do
- pending_job.update_column(:status, :running)
- end
+ context 'when a conflicting data is stored in denormalized table' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }
- it 'removes queuing entry upon build assignment attempt' do
- expect(pending_job.reload).to be_running
- expect(pending_job.queuing_entry).to be_present
+ before do
+ pending_job.update_column(:status, :running)
+ end
- expect(execute).not_to be_valid
- expect(pending_job.reload.queuing_entry).not_to be_present
- end
+ it 'removes queuing entry upon build assignment attempt' do
+ expect(pending_job.reload).to be_running
+ expect(pending_job.queuing_entry).to be_present
+
+ expect(execute).not_to be_valid
+ expect(pending_job.reload.queuing_entry).not_to be_present
end
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 3d1abe290bc..ea15e3ea2c0 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupService do
+RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupService, feature_category: :continuous_integration do
include ConcurrentHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 07518c35fab..51a894640ab 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RetryPipelineService, '#execute' do
+RSpec.describe Ci::RetryPipelineService, '#execute', feature_category: :continuous_integration do
include ProjectForksHelper
let_it_be_with_refind(:user) { create(:user) }
diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb
index 27d25e88944..e3ae1d93e5e 100644
--- a/spec/services/ci/run_scheduled_build_service_spec.rb
+++ b/spec/services/ci/run_scheduled_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RunScheduledBuildService do
+RSpec.describe Ci::RunScheduledBuildService, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb
index 673bf3ef90e..52acfcbb7af 100644
--- a/spec/services/ci/runners/create_runner_service_spec.rb
+++ b/spec/services/ci/runners/create_runner_service_spec.rb
@@ -83,6 +83,49 @@ RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category:
expect(runner.authenticated_user_registration_type?).to be_truthy
expect(runner.runner_type).to eq 'instance_type'
end
+
+ context 'with a nil paused value' do
+ let(:args) do
+ {
+ paused: nil,
+ description: 'some description',
+ maintenance_note: 'a note',
+ tag_list: %w[tag1 tag2],
+ access_level: 'ref_protected',
+ locked: true,
+ maximum_timeout: 600,
+ run_untagged: false
+ }
+ end
+
+ it { is_expected.to be_success }
+
+ it 'creates runner with active set to true' do
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.active).to eq true
+ end
+ end
+
+ context 'with no paused value given' do
+ let(:args) do
+ {
+ description: 'some description',
+ maintenance_note: 'a note',
+ tag_list: %w[tag1 tag2],
+ access_level: 'ref_protected',
+ locked: true,
+ maximum_timeout: 600,
+ run_untagged: false
+ }
+ end
+
+ it { is_expected.to be_success }
+
+ it 'creates runner with active set to true' do
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.active).to eq true
+ end
+ end
end
end
diff --git a/spec/services/ci/runners/process_runner_version_update_service_spec.rb b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
index e62cb1ec3e3..f8b7aa281af 100644
--- a/spec/services/ci/runners/process_runner_version_update_service_spec.rb
+++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
@@ -29,6 +29,19 @@ RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService, feature_category:
end
end
+ context 'when fetching runner releases is disabled' do
+ before do
+ stub_application_setting(update_runner_versions_enabled: false)
+ end
+
+ it 'does not update ci_runner_versions records', :aggregate_failures do
+ expect do
+ expect(execute).to be_error
+ expect(execute.message).to eq 'version update disabled'
+ end.not_to change(Ci::RunnerVersion, :count).from(0)
+ end
+ end
+
context 'with successful result from upgrade check' do
before do
url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
diff --git a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
index a452a65829a..6d91f5098eb 100644
--- a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::StuckBuilds::DropPendingService do
+RSpec.describe Ci::StuckBuilds::DropPendingService, feature_category: :runner_fleet do
let_it_be(:runner) { create(:ci_runner) }
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
let_it_be_with_reload(:job) do
diff --git a/spec/services/ci/stuck_builds/drop_running_service_spec.rb b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
index c1c92c2b8e2..deb807753c2 100644
--- a/spec/services/ci/stuck_builds/drop_running_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_running_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::StuckBuilds::DropRunningService do
+RSpec.describe Ci::StuckBuilds::DropRunningService, feature_category: :runner_fleet do
let!(:runner) { create :ci_runner }
let!(:job) { create(:ci_build, runner: runner, created_at: created_at, updated_at: updated_at, status: status) }
diff --git a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
index a4f9f97fffc..f2e658c3ae3 100644
--- a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::StuckBuilds::DropScheduledService do
+RSpec.describe Ci::StuckBuilds::DropScheduledService, feature_category: :runner_fleet do
let_it_be(:runner) { create :ci_runner }
let!(:job) { create :ci_build, :scheduled, scheduled_at: scheduled_at, runner: runner }
diff --git a/spec/services/ci/test_failure_history_service_spec.rb b/spec/services/ci/test_failure_history_service_spec.rb
index 10f6c6f5007..e77c6533483 100644
--- a/spec/services/ci/test_failure_history_service_spec.rb
+++ b/spec/services/ci/test_failure_history_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
+RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be_with_reload(:pipeline) do
diff --git a/spec/services/ci/track_failed_build_service_spec.rb b/spec/services/ci/track_failed_build_service_spec.rb
index 676769d2fc7..23e7cee731d 100644
--- a/spec/services/ci/track_failed_build_service_spec.rb
+++ b/spec/services/ci/track_failed_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::TrackFailedBuildService do
+RSpec.describe Ci::TrackFailedBuildService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
diff --git a/spec/services/ci/unlock_artifacts_service_spec.rb b/spec/services/ci/unlock_artifacts_service_spec.rb
index c15e1cb2b5d..1921ea4bdba 100644
--- a/spec/services/ci/unlock_artifacts_service_spec.rb
+++ b/spec/services/ci/unlock_artifacts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UnlockArtifactsService do
+RSpec.describe Ci::UnlockArtifactsService, feature_category: :continuous_integration do
using RSpec::Parameterized::TableSyntax
where(:tag) do
@@ -24,7 +24,7 @@ RSpec.describe Ci::UnlockArtifactsService do
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!(:child_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, child_of: pipeline, project: project, locked: :artifacts_locked) }
let!(:newer_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: ref, tag: tag, project: project, locked: :artifacts_locked) }
let!(:other_ref_pipeline) { create(:ci_pipeline, :with_persisted_artifacts, ref: 'other_ref', tag: tag, project: project, locked: :artifacts_locked) }
let!(:sources_pipeline) { create(:ci_sources_pipeline, source_job: source_job, source_project: project, pipeline: child_pipeline, project: project) }
@@ -201,8 +201,7 @@ RSpec.describe Ci::UnlockArtifactsService do
describe '#unlock_job_artifacts_query' do
subject { described_class.new(pipeline.project, pipeline.user).unlock_job_artifacts_query(pipeline_ids) }
- context 'when running on a ref before a pipeline' do
- let(:before_pipeline) { pipeline }
+ context 'when given a single pipeline ID' do
let(:pipeline_ids) { [older_pipeline.id] }
it 'produces the expected SQL string' do
@@ -226,8 +225,7 @@ RSpec.describe Ci::UnlockArtifactsService do
end
end
- context 'when running on just the ref' do
- let(:before_pipeline) { nil }
+ context 'when given multiple pipeline IDs' do
let(:pipeline_ids) { [older_pipeline.id, newer_pipeline.id, pipeline.id] }
it 'produces the expected SQL string' do
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index dd26339831c..4fd4492278d 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UpdateBuildQueueService do
+RSpec.describe Ci::UpdateBuildQueueService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/services/ci/update_instance_variables_service_spec.rb b/spec/services/ci/update_instance_variables_service_spec.rb
index f235d006e34..19f28793f90 100644
--- a/spec/services/ci/update_instance_variables_service_spec.rb
+++ b/spec/services/ci/update_instance_variables_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UpdateInstanceVariablesService do
+RSpec.describe Ci::UpdateInstanceVariablesService, feature_category: :pipeline_composition do
let(:params) { { variables_attributes: variables_attributes } }
subject { described_class.new(params) }
diff --git a/spec/services/ci/update_pending_build_service_spec.rb b/spec/services/ci/update_pending_build_service_spec.rb
index e49b22299f0..abf31dd5184 100644
--- a/spec/services/ci/update_pending_build_service_spec.rb
+++ b/spec/services/ci/update_pending_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UpdatePendingBuildService do
+RSpec.describe Ci::UpdatePendingBuildService, feature_category: :continuous_integration do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be_with_reload(:pending_build_1) { create(:ci_pending_build, project: project, instance_runners_enabled: false) }
diff --git a/spec/services/clusters/agent_tokens/create_service_spec.rb b/spec/services/clusters/agent_tokens/create_service_spec.rb
index dc7abd1504b..519a3ba7ce5 100644
--- a/spec/services/clusters/agent_tokens/create_service_spec.rb
+++ b/spec/services/clusters/agent_tokens/create_service_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-RSpec.describe Clusters::AgentTokens::CreateService do
- subject(:service) { described_class.new(container: project, current_user: user, params: params) }
+RSpec.describe Clusters::AgentTokens::CreateService, feature_category: :kubernetes_management do
+ subject(:service) { described_class.new(agent: cluster_agent, current_user: user, params: params) }
let_it_be(:user) { create(:user) }
let(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
- let(:params) { { agent_id: cluster_agent.id, description: 'token description', name: 'token name' } }
+ let(:params) { { description: 'token description', name: 'token name' } }
describe '#execute' do
subject { service.execute }
@@ -75,7 +75,7 @@ RSpec.describe Clusters::AgentTokens::CreateService do
it 'returns validation errors', :aggregate_failures do
expect(subject.status).to eq(:error)
- expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
+ expect(subject.message).to eq(["Name can't be blank"])
end
end
end
diff --git a/spec/services/clusters/agent_tokens/revoke_service_spec.rb b/spec/services/clusters/agent_tokens/revoke_service_spec.rb
new file mode 100644
index 00000000000..9e511de0a13
--- /dev/null
+++ b/spec/services/clusters/agent_tokens/revoke_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentTokens::RevokeService, feature_category: :kubernetes_management do
+ describe '#execute' do
+ subject { described_class.new(token: agent_token, current_user: user).execute }
+
+ let(:agent) { create(:cluster_agent) }
+ let(:agent_token) { create(:cluster_agent_token, agent: agent) }
+ let(:project) { agent.project }
+ let(:user) { agent.created_by_user }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when user is authorized' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when user revokes agent token' do
+ it 'succeeds' do
+ subject
+
+ expect(agent_token.revoked?).to be true
+ end
+
+ it 'creates an activity event' do
+ expect { subject }.to change { ::Clusters::Agents::ActivityEvent.count }.by(1)
+
+ event = agent.activity_events.last
+
+ expect(event).to have_attributes(
+ kind: 'token_revoked',
+ level: 'info',
+ recorded_at: agent_token.reload.updated_at,
+ user: user,
+ agent_token: agent_token
+ )
+ end
+ end
+
+ context 'when there is a validation failure' do
+ before do
+ agent_token.name = '' # make the record invalid, as we require a name to be present
+ end
+
+ it 'fails without raising an error', :aggregate_failures do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq(["Name can't be blank"])
+ end
+
+ it 'does not create an activity event' do
+ expect { subject }.not_to change { ::Clusters::Agents::ActivityEvent.count }
+ end
+ end
+ end
+
+ context 'when user is not authorized' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ context 'when user attempts to revoke agent token' do
+ it 'fails' do
+ subject
+
+ expect(agent_token.revoked?).to be false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/agent_tokens/track_usage_service_spec.rb b/spec/services/clusters/agent_tokens/track_usage_service_spec.rb
index 3350b15a5ce..e9e1a5f7ad9 100644
--- a/spec/services/clusters/agent_tokens/track_usage_service_spec.rb
+++ b/spec/services/clusters/agent_tokens/track_usage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::AgentTokens::TrackUsageService do
+RSpec.describe Clusters::AgentTokens::TrackUsageService, feature_category: :kubernetes_management do
let_it_be(:agent) { create(:cluster_agent) }
describe '#execute', :clean_gitlab_redis_cache do
diff --git a/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb b/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb
new file mode 100644
index 00000000000..c099d87f6eb
--- /dev/null
+++ b/spec/services/clusters/agents/authorize_proxy_user_service_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::AuthorizeProxyUserService, feature_category: :kubernetes_management do
+ subject(:service_response) { service.execute }
+
+ let(:service) { described_class.new(user, agent) }
+ let(:user) { create(:user) }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user_access_config) do
+ {
+ 'user_access' => {
+ 'access_as' => { 'agent' => {} },
+ 'projects' => [{ 'id' => project.full_path }],
+ 'groups' => [{ 'id' => group.full_path }]
+ }
+ }
+ end
+
+ let_it_be(:configuration_project) do
+ create(
+ :project, :custom_repo,
+ files: {
+ ".gitlab/agents/the-agent/config.yaml" => user_access_config.to_yaml
+ }
+ )
+ end
+
+ let_it_be(:agent) { create(:cluster_agent, name: 'the-agent', project: configuration_project) }
+
+ it 'returns forbidden when user has no access to any project', :aggregate_failures do
+ expect(service_response).to be_error
+ expect(service_response.reason).to eq :forbidden
+ end
+
+ context 'when user is member of an authorized group' do
+ it 'authorizes developers', :aggregate_failures do
+ group.add_member(user, :developer)
+ expect(service_response).to be_success
+ expect(service_response.payload[:user]).to include(id: user.id, username: user.username)
+ expect(service_response.payload[:agent]).to include(id: agent.id, config_project: { id: agent.project.id })
+ end
+
+ it 'does not authorize reporters', :aggregate_failures do
+ group.add_member(user, :reporter)
+ expect(service_response).to be_error
+ expect(service_response.reason).to eq :forbidden
+ end
+ end
+
+ context 'when user is member of an authorized project' do
+ it 'authorizes developers', :aggregate_failures do
+ project.add_member(user, :developer)
+ expect(service_response).to be_success
+ expect(service_response.payload[:user]).to include(id: user.id, username: user.username)
+ expect(service_response.payload[:agent]).to include(id: agent.id, config_project: { id: agent.project.id })
+ end
+
+ it 'does not authorize reporters', :aggregate_failures do
+ project.add_member(user, :reporter)
+ expect(service_response).to be_error
+ expect(service_response.reason).to eq :forbidden
+ end
+ end
+end
diff --git a/spec/services/clusters/agents/create_activity_event_service_spec.rb b/spec/services/clusters/agents/create_activity_event_service_spec.rb
index 7a8f0e16d60..3da8ecddb8d 100644
--- a/spec/services/clusters/agents/create_activity_event_service_spec.rb
+++ b/spec/services/clusters/agents/create_activity_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::CreateActivityEventService do
+RSpec.describe Clusters::Agents::CreateActivityEventService, feature_category: :kubernetes_management do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:token) { create(:cluster_agent_token, agent: agent) }
let_it_be(:user) { create(:user) }
@@ -40,5 +40,16 @@ RSpec.describe Clusters::Agents::CreateActivityEventService do
subject
end
+
+ context 'when activity event creation fails' do
+ let(:params) { {} }
+
+ it 'tracks the exception without raising' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(instance_of(ActiveRecord::RecordInvalid), agent_id: agent.id)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/clusters/agents/create_service_spec.rb b/spec/services/clusters/agents/create_service_spec.rb
index 2b3bbcae13c..dc69dfb5e27 100644
--- a/spec/services/clusters/agents/create_service_spec.rb
+++ b/spec/services/clusters/agents/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::CreateService do
+RSpec.describe Clusters::Agents::CreateService, feature_category: :kubernetes_management do
subject(:service) { described_class.new(project, user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/services/clusters/agents/delete_expired_events_service_spec.rb b/spec/services/clusters/agents/delete_expired_events_service_spec.rb
index 3dc166f54eb..892cd5a70ea 100644
--- a/spec/services/clusters/agents/delete_expired_events_service_spec.rb
+++ b/spec/services/clusters/agents/delete_expired_events_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::DeleteExpiredEventsService do
+RSpec.describe Clusters::Agents::DeleteExpiredEventsService, feature_category: :kubernetes_management do
let_it_be(:agent) { create(:cluster_agent) }
describe '#execute' do
diff --git a/spec/services/clusters/agents/delete_service_spec.rb b/spec/services/clusters/agents/delete_service_spec.rb
index abe1bdaab27..da97cdee4ca 100644
--- a/spec/services/clusters/agents/delete_service_spec.rb
+++ b/spec/services/clusters/agents/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::DeleteService do
+RSpec.describe Clusters::Agents::DeleteService, feature_category: :kubernetes_management do
subject(:service) { described_class.new(container: project, current_user: user) }
let(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/services/clusters/build_kubernetes_namespace_service_spec.rb b/spec/services/clusters/build_kubernetes_namespace_service_spec.rb
index 4ee933374f6..b1be3eb4199 100644
--- a/spec/services/clusters/build_kubernetes_namespace_service_spec.rb
+++ b/spec/services/clusters/build_kubernetes_namespace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::BuildKubernetesNamespaceService do
+RSpec.describe Clusters::BuildKubernetesNamespaceService, feature_category: :kubernetes_management do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:environment) { create(:environment) }
let(:project) { environment.project }
diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb
index c7a64435d3b..9e71b7a8115 100644
--- a/spec/services/clusters/build_service_spec.rb
+++ b/spec/services/clusters/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::BuildService do
+RSpec.describe Clusters::BuildService, feature_category: :kubernetes_management do
describe '#execute' do
subject { described_class.new(cluster_subject).execute }
diff --git a/spec/services/clusters/cleanup/project_namespace_service_spec.rb b/spec/services/clusters/cleanup/project_namespace_service_spec.rb
index 8d3ae217a9f..366e4fa9c03 100644
--- a/spec/services/clusters/cleanup/project_namespace_service_spec.rb
+++ b/spec/services/clusters/cleanup/project_namespace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Cleanup::ProjectNamespaceService do
+RSpec.describe Clusters::Cleanup::ProjectNamespaceService, feature_category: :kubernetes_management do
describe '#execute' do
subject { service.execute }
diff --git a/spec/services/clusters/cleanup/service_account_service_spec.rb b/spec/services/clusters/cleanup/service_account_service_spec.rb
index 769762237f9..881ec85b3d5 100644
--- a/spec/services/clusters/cleanup/service_account_service_spec.rb
+++ b/spec/services/clusters/cleanup/service_account_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Cleanup::ServiceAccountService do
+RSpec.describe Clusters::Cleanup::ServiceAccountService, feature_category: :kubernetes_management do
describe '#execute' do
subject { service.execute }
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 95f10cdbd80..0d170f66f4a 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::CreateService do
+RSpec.describe Clusters::CreateService, feature_category: :kubernetes_management do
let(:access_token) { 'xxx' }
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/services/clusters/destroy_service_spec.rb b/spec/services/clusters/destroy_service_spec.rb
index dc600c9e830..2bc0099ff04 100644
--- a/spec/services/clusters/destroy_service_spec.rb
+++ b/spec/services/clusters/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::DestroyService do
+RSpec.describe Clusters::DestroyService, feature_category: :kubernetes_management do
describe '#execute' do
subject { described_class.new(cluster.user, params).execute(cluster) }
diff --git a/spec/services/clusters/integrations/create_service_spec.rb b/spec/services/clusters/integrations/create_service_spec.rb
index 9104e07504d..fa47811dc6b 100644
--- a/spec/services/clusters/integrations/create_service_spec.rb
+++ b/spec/services/clusters/integrations/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Integrations::CreateService, '#execute' do
+RSpec.describe Clusters::Integrations::CreateService, '#execute', feature_category: :kubernetes_management do
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
diff --git a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
index 526462931a6..2d527bb0872 100644
--- a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
+++ b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Integrations::PrometheusHealthCheckService, '#execute' do
+RSpec.describe Clusters::Integrations::PrometheusHealthCheckService, '#execute', feature_category: :kubernetes_management do
let(:service) { described_class.new(cluster) }
subject { service.execute }
diff --git a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
index 90956e7b4ea..8ae34e4f9ab 100644
--- a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
+RSpec.describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute', feature_category: :kubernetes_management do
include KubernetesHelpers
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index 37478a0bcd9..bdf46c19e36 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
+RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService, feature_category: :kubernetes_management do
include KubernetesHelpers
let(:api_url) { 'http://111.111.111.111' }
diff --git a/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb
index 03c402fb066..2b77df1eb6d 100644
--- a/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb
+++ b/spec/services/clusters/kubernetes/fetch_kubernetes_token_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Kubernetes::FetchKubernetesTokenService do
+RSpec.describe Clusters::Kubernetes::FetchKubernetesTokenService, feature_category: :kubernetes_management do
include KubernetesHelpers
describe '#execute' do
diff --git a/spec/services/clusters/kubernetes_spec.rb b/spec/services/clusters/kubernetes_spec.rb
index 12af63890fc..7e22c2f95df 100644
--- a/spec/services/clusters/kubernetes_spec.rb
+++ b/spec/services/clusters/kubernetes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Kubernetes do
+RSpec.describe Clusters::Kubernetes, feature_category: :kubernetes_management do
it { is_expected.to be_const_defined(:GITLAB_SERVICE_ACCOUNT_NAME) }
it { is_expected.to be_const_defined(:GITLAB_SERVICE_ACCOUNT_NAMESPACE) }
it { is_expected.to be_const_defined(:GITLAB_ADMIN_TOKEN_NAME) }
diff --git a/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb b/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
index a21c378d3d1..8a49d90aa48 100644
--- a/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
+++ b/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Management::ValidateManagementProjectPermissionsService do
+RSpec.describe Clusters::Management::ValidateManagementProjectPermissionsService, feature_category: :kubernetes_management do
describe '#execute' do
subject { described_class.new(user).execute(cluster, management_project_id) }
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index 9aead97f41c..31661d30f41 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::UpdateService do
+RSpec.describe Clusters::UpdateService, feature_category: :kubernetes_management do
include KubernetesHelpers
describe '#execute' do
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
index dce8d4f80f2..ab53bcf8657 100644
--- a/spec/services/cohorts_service_spec.rb
+++ b/spec/services/cohorts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CohortsService do
+RSpec.describe CohortsService, feature_category: :shared do
describe '#execute' do
def month_start(months_ago)
months_ago.months.ago.beginning_of_month.to_date
diff --git a/spec/services/commits/cherry_pick_service_spec.rb b/spec/services/commits/cherry_pick_service_spec.rb
index 2565e17ac90..880ebea1c09 100644
--- a/spec/services/commits/cherry_pick_service_spec.rb
+++ b/spec/services/commits/cherry_pick_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Commits::CherryPickService do
+RSpec.describe Commits::CherryPickService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
# * ddd0f15ae83993f5cb66a927a28673882e99100b (HEAD -> master, origin/master, origin/HEAD) Merge branch 'po-fix-test-en
# |\
diff --git a/spec/services/commits/commit_patch_service_spec.rb b/spec/services/commits/commit_patch_service_spec.rb
index edd0918e488..a9d61be23be 100644
--- a/spec/services/commits/commit_patch_service_spec.rb
+++ b/spec/services/commits/commit_patch_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Commits::CommitPatchService do
+RSpec.describe Commits::CommitPatchService, feature_category: :source_code_management do
describe '#execute' do
let(:patches) do
patches_folder = Rails.root.join('spec/fixtures/patchfiles')
diff --git a/spec/services/commits/tag_service_spec.rb b/spec/services/commits/tag_service_spec.rb
index dd742ebe469..25aa84276c3 100644
--- a/spec/services/commits/tag_service_spec.rb
+++ b/spec/services/commits/tag_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Commits::TagService do
+RSpec.describe Commits::TagService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index e96a7f2f4f4..6757fbdf5d4 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CompareService do
+RSpec.describe CompareService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, 'feature') }
diff --git a/spec/services/concerns/audit_event_save_type_spec.rb b/spec/services/concerns/audit_event_save_type_spec.rb
index fbaebd9f85c..a89eb513d27 100644
--- a/spec/services/concerns/audit_event_save_type_spec.rb
+++ b/spec/services/concerns/audit_event_save_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuditEventSaveType do
+RSpec.describe AuditEventSaveType, feature_category: :audit_events do
subject(:target) { Object.new.extend(described_class) }
describe '#should_save_database? and #should_save_stream?' do
diff --git a/spec/services/concerns/exclusive_lease_guard_spec.rb b/spec/services/concerns/exclusive_lease_guard_spec.rb
index 6a2aa0a377b..b081d2642c0 100644
--- a/spec/services/concerns/exclusive_lease_guard_spec.rb
+++ b/spec/services/concerns/exclusive_lease_guard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ExclusiveLeaseGuard, :clean_gitlab_redis_shared_state do
+RSpec.describe ExclusiveLeaseGuard, :clean_gitlab_redis_shared_state, feature_category: :shared do
subject :subject_class do
Class.new do
include ExclusiveLeaseGuard
diff --git a/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb b/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb
index 5b1e8fca31b..c6ee5b78c13 100644
--- a/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb
+++ b/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AssignsMergeParams do
+RSpec.describe MergeRequests::AssignsMergeParams, feature_category: :code_review_workflow do
it 'raises an error when used from an instance that does not respond to #current_user' do
define_class = -> { Class.new { include MergeRequests::AssignsMergeParams }.new }
diff --git a/spec/services/concerns/rate_limited_service_spec.rb b/spec/services/concerns/rate_limited_service_spec.rb
index d913cd17067..2172c756ecf 100644
--- a/spec/services/concerns/rate_limited_service_spec.rb
+++ b/spec/services/concerns/rate_limited_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RateLimitedService do
+RSpec.describe RateLimitedService, feature_category: :rate_limiting do
let(:key) { :issues_create }
let(:scope) { [:container, :current_user] }
let(:opts) { { scope: scope, users_allowlist: -> { [User.support_bot.username] } } }
diff --git a/spec/services/container_expiration_policies/cleanup_service_spec.rb b/spec/services/container_expiration_policies/cleanup_service_spec.rb
index 6e1be7271e1..4663944f0b9 100644
--- a/spec/services/container_expiration_policies/cleanup_service_spec.rb
+++ b/spec/services/container_expiration_policies/cleanup_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerExpirationPolicies::CleanupService do
+RSpec.describe ContainerExpirationPolicies::CleanupService, feature_category: :container_registry do
let_it_be(:repository, reload: true) { create(:container_repository, expiration_policy_started_at: 30.minutes.ago) }
let_it_be(:project) { repository.project }
@@ -190,6 +190,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
context 'with only the current repository started_at before the policy next_run_at' do
before do
+ repository.update!(expiration_policy_started_at: policy.next_run_at + 9.minutes)
repository2.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes)
repository3.update!(expiration_policy_started_at: policy.next_run_at + 12.minutes)
end
diff --git a/spec/services/container_expiration_policies/update_service_spec.rb b/spec/services/container_expiration_policies/update_service_spec.rb
index 7d949b77de7..992240201e0 100644
--- a/spec/services/container_expiration_policies/update_service_spec.rb
+++ b/spec/services/container_expiration_policies/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerExpirationPolicies::UpdateService do
+RSpec.describe ContainerExpirationPolicies::UpdateService, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project) }
diff --git a/spec/services/customer_relations/contacts/create_service_spec.rb b/spec/services/customer_relations/contacts/create_service_spec.rb
index db6cce799fe..610143586e3 100644
--- a/spec/services/customer_relations/contacts/create_service_spec.rb
+++ b/spec/services/customer_relations/contacts/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CustomerRelations::Contacts::CreateService do
+RSpec.describe CustomerRelations::Contacts::CreateService, feature_category: :service_desk do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:not_found_or_does_not_belong) { 'The specified organization was not found or does not belong to this group' }
diff --git a/spec/services/customer_relations/contacts/update_service_spec.rb b/spec/services/customer_relations/contacts/update_service_spec.rb
index 729fdc2058b..105b5bad5f7 100644
--- a/spec/services/customer_relations/contacts/update_service_spec.rb
+++ b/spec/services/customer_relations/contacts/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CustomerRelations::Contacts::UpdateService do
+RSpec.describe CustomerRelations::Contacts::UpdateService, feature_category: :service_desk do
let_it_be(:user) { create(:user) }
let(:contact) { create(:contact, first_name: 'Mark', group: group, state: 'active') }
diff --git a/spec/services/customer_relations/organizations/create_service_spec.rb b/spec/services/customer_relations/organizations/create_service_spec.rb
index 18eefdd716e..3b1ae5606b1 100644
--- a/spec/services/customer_relations/organizations/create_service_spec.rb
+++ b/spec/services/customer_relations/organizations/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CustomerRelations::Organizations::CreateService do
+RSpec.describe CustomerRelations::Organizations::CreateService, feature_category: :service_desk do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/customer_relations/organizations/update_service_spec.rb b/spec/services/customer_relations/organizations/update_service_spec.rb
index 4764ba85551..ac71a211bf8 100644
--- a/spec/services/customer_relations/organizations/update_service_spec.rb
+++ b/spec/services/customer_relations/organizations/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CustomerRelations::Organizations::UpdateService do
+RSpec.describe CustomerRelations::Organizations::UpdateService, feature_category: :service_desk do
let_it_be(:user) { create(:user) }
let(:organization) { create(:organization, name: 'Test', group: group, state: 'active') }
diff --git a/spec/services/database/consistency_fix_service_spec.rb b/spec/services/database/consistency_fix_service_spec.rb
index 9a0fac2191c..fcc776cbc2a 100644
--- a/spec/services/database/consistency_fix_service_spec.rb
+++ b/spec/services/database/consistency_fix_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Database::ConsistencyFixService do
+RSpec.describe Database::ConsistencyFixService, feature_category: :pods do
describe '#execute' do
context 'fixing namespaces inconsistencies' do
subject(:consistency_fix_service) do
diff --git a/spec/services/dependency_proxy/auth_token_service_spec.rb b/spec/services/dependency_proxy/auth_token_service_spec.rb
index c686f57c5cb..2612c5765a4 100644
--- a/spec/services/dependency_proxy/auth_token_service_spec.rb
+++ b/spec/services/dependency_proxy/auth_token_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DependencyProxy::AuthTokenService do
+RSpec.describe DependencyProxy::AuthTokenService, feature_category: :dependency_proxy do
include DependencyProxyHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
index 470c6eb9e03..13620b3dfc1 100644
--- a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DependencyProxy::FindCachedManifestService do
+RSpec.describe DependencyProxy::FindCachedManifestService, feature_category: :dependency_proxy do
include DependencyProxyHelpers
let_it_be(:image) { 'alpine' }
diff --git a/spec/services/dependency_proxy/group_settings/update_service_spec.rb b/spec/services/dependency_proxy/group_settings/update_service_spec.rb
index 4954d9ec267..38f837a828a 100644
--- a/spec/services/dependency_proxy/group_settings/update_service_spec.rb
+++ b/spec/services/dependency_proxy/group_settings/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::DependencyProxy::GroupSettings::UpdateService do
+RSpec.describe ::DependencyProxy::GroupSettings::UpdateService, feature_category: :dependency_proxy do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:group) { create(:group) }
diff --git a/spec/services/dependency_proxy/head_manifest_service_spec.rb b/spec/services/dependency_proxy/head_manifest_service_spec.rb
index 949a8eb3bee..a9646a185bc 100644
--- a/spec/services/dependency_proxy/head_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/head_manifest_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DependencyProxy::HeadManifestService do
+RSpec.describe DependencyProxy::HeadManifestService, feature_category: :dependency_proxy do
include DependencyProxyHelpers
let(:image) { 'alpine' }
diff --git a/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb b/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb
index 3a6ba2cca71..f58434222a5 100644
--- a/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb
+++ b/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::DependencyProxy::ImageTtlGroupPolicies::UpdateService do
+RSpec.describe ::DependencyProxy::ImageTtlGroupPolicies::UpdateService, feature_category: :dependency_proxy do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:group) { create(:group) }
diff --git a/spec/services/dependency_proxy/request_token_service_spec.rb b/spec/services/dependency_proxy/request_token_service_spec.rb
index 8b3ba783b8d..0cc3695f0b0 100644
--- a/spec/services/dependency_proxy/request_token_service_spec.rb
+++ b/spec/services/dependency_proxy/request_token_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DependencyProxy::RequestTokenService do
+RSpec.describe DependencyProxy::RequestTokenService, feature_category: :dependency_proxy do
include DependencyProxyHelpers
let(:image) { 'alpine:3.9' }
diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb
index 2e3318236f5..8bff80b2d11 100644
--- a/spec/services/deploy_keys/create_service_spec.rb
+++ b/spec/services/deploy_keys/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeployKeys::CreateService do
+RSpec.describe DeployKeys::CreateService, feature_category: :continuous_delivery do
let(:user) { create(:user) }
let(:params) { attributes_for(:deploy_key) }
diff --git a/spec/services/deployments/archive_in_project_service_spec.rb b/spec/services/deployments/archive_in_project_service_spec.rb
index a316c210d64..ed03ce06255 100644
--- a/spec/services/deployments/archive_in_project_service_spec.rb
+++ b/spec/services/deployments/archive_in_project_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::ArchiveInProjectService do
+RSpec.describe Deployments::ArchiveInProjectService, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :repository) }
let(:service) { described_class.new(project, nil) }
diff --git a/spec/services/deployments/create_for_build_service_spec.rb b/spec/services/deployments/create_for_build_service_spec.rb
index 3748df87d99..c07fc07cfbf 100644
--- a/spec/services/deployments/create_for_build_service_spec.rb
+++ b/spec/services/deployments/create_for_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::CreateForBuildService do
+RSpec.describe Deployments::CreateForBuildService, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb
index 0f2a6ce32e1..2a70d450575 100644
--- a/spec/services/deployments/create_service_spec.rb
+++ b/spec/services/deployments/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::CreateService do
+RSpec.describe Deployments::CreateService, feature_category: :continuous_delivery do
let(:user) { create(:user) }
describe '#execute' do
diff --git a/spec/services/deployments/link_merge_requests_service_spec.rb b/spec/services/deployments/link_merge_requests_service_spec.rb
index a653cd2b48b..a468af90ffb 100644
--- a/spec/services/deployments/link_merge_requests_service_spec.rb
+++ b/spec/services/deployments/link_merge_requests_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::LinkMergeRequestsService do
+RSpec.describe Deployments::LinkMergeRequestsService, feature_category: :continuous_delivery do
let(:project) { create(:project, :repository) }
# * ddd0f15 Merge branch 'po-fix-test-env-path' into 'master'
diff --git a/spec/services/deployments/older_deployments_drop_service_spec.rb b/spec/services/deployments/older_deployments_drop_service_spec.rb
index d9a512a5dd2..7e3074a1688 100644
--- a/spec/services/deployments/older_deployments_drop_service_spec.rb
+++ b/spec/services/deployments/older_deployments_drop_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::OlderDeploymentsDropService do
+RSpec.describe Deployments::OlderDeploymentsDropService, feature_category: :continuous_delivery do
let(:environment) { create(:environment) }
let(:deployment) { create(:deployment, environment: environment) }
let(:service) { described_class.new(deployment) }
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index 31a3abda8c7..33c9c9ed592 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::UpdateEnvironmentService do
+RSpec.describe Deployments::UpdateEnvironmentService, feature_category: :continuous_delivery do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:options) { { name: environment_name } }
diff --git a/spec/services/deployments/update_service_spec.rb b/spec/services/deployments/update_service_spec.rb
index d3840189ba4..0814091765c 100644
--- a/spec/services/deployments/update_service_spec.rb
+++ b/spec/services/deployments/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::UpdateService do
+RSpec.describe Deployments::UpdateService, feature_category: :continuous_delivery do
let(:deploy) { create(:deployment) }
describe '#execute' do
diff --git a/spec/services/design_management/copy_design_collection/copy_service_spec.rb b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
index 89a78c9bf5f..048327792e0 100644
--- a/spec/services/design_management/copy_design_collection/copy_service_spec.rb
+++ b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitlab_redis_shared_state do
+RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitlab_redis_shared_state, feature_category: :portfolio_management do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user) }
@@ -117,6 +117,7 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
new_designs.zip(old_designs).each do |new_design, old_design|
expect(new_design).to have_attributes(
filename: old_design.filename,
+ description: old_design.description,
relative_position: old_design.relative_position,
issue: target_issue,
project: target_issue.project
diff --git a/spec/services/design_management/copy_design_collection/queue_service_spec.rb b/spec/services/design_management/copy_design_collection/queue_service_spec.rb
index 05a7b092ccf..e6809e65d8a 100644
--- a/spec/services/design_management/copy_design_collection/queue_service_spec.rb
+++ b/spec/services/design_management/copy_design_collection/queue_service_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DesignManagement::CopyDesignCollection::QueueService, :clean_gitlab_redis_shared_state do
+RSpec.describe DesignManagement::CopyDesignCollection::QueueService, :clean_gitlab_redis_shared_state,
+ feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/design_management/delete_designs_service_spec.rb b/spec/services/design_management/delete_designs_service_spec.rb
index 48e53a92758..09ff2c5b897 100644
--- a/spec/services/design_management/delete_designs_service_spec.rb
+++ b/spec/services/design_management/delete_designs_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DesignManagement::DeleteDesignsService do
+RSpec.describe DesignManagement::DeleteDesignsService, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/services/design_management/design_user_notes_count_service_spec.rb b/spec/services/design_management/design_user_notes_count_service_spec.rb
index 37806d3461c..1dbd055038c 100644
--- a/spec/services/design_management/design_user_notes_count_service_spec.rb
+++ b/spec/services/design_management/design_user_notes_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DesignManagement::DesignUserNotesCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe DesignManagement::DesignUserNotesCountService, :use_clean_rails_memory_store_caching, feature_category: :design_management do
let_it_be(:design) { create(:design, :with_file) }
subject { described_class.new(design) }
diff --git a/spec/services/design_management/generate_image_versions_service_spec.rb b/spec/services/design_management/generate_image_versions_service_spec.rb
index 5409ec12016..08442f221fa 100644
--- a/spec/services/design_management/generate_image_versions_service_spec.rb
+++ b/spec/services/design_management/generate_image_versions_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DesignManagement::GenerateImageVersionsService do
+RSpec.describe DesignManagement::GenerateImageVersionsService, feature_category: :design_management do
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:version) { create(:design, :with_lfs_file, issue: issue).versions.first }
diff --git a/spec/services/design_management/move_designs_service_spec.rb b/spec/services/design_management/move_designs_service_spec.rb
index 519378a8dd4..8276d8d186a 100644
--- a/spec/services/design_management/move_designs_service_spec.rb
+++ b/spec/services/design_management/move_designs_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DesignManagement::MoveDesignsService do
+RSpec.describe DesignManagement::MoveDesignsService, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:issue) { create(:issue) }
diff --git a/spec/services/discussions/capture_diff_note_position_service_spec.rb b/spec/services/discussions/capture_diff_note_position_service_spec.rb
index 11614ccfd55..313e828bf0a 100644
--- a/spec/services/discussions/capture_diff_note_position_service_spec.rb
+++ b/spec/services/discussions/capture_diff_note_position_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Discussions::CaptureDiffNotePositionService do
+RSpec.describe Discussions::CaptureDiffNotePositionService, feature_category: :code_review_workflow do
subject { described_class.new(note.noteable, paths) }
context 'image note on diff' do
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 8ba54495d4c..96922535eb2 100644
--- a/spec/services/discussions/capture_diff_note_positions_service_spec.rb
+++ b/spec/services/discussions/capture_diff_note_positions_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Discussions::CaptureDiffNotePositionsService do
+RSpec.describe Discussions::CaptureDiffNotePositionsService, feature_category: :code_review_workflow do
context 'when merge request has a discussion' do
let(:source_branch) { 'compare-with-merge-head-source' }
let(:target_branch) { 'compare-with-merge-head-target' }
diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index e7a3505bbd4..274430fdccb 100644
--- a/spec/services/discussions/update_diff_position_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Discussions::UpdateDiffPositionService do
+RSpec.describe Discussions::UpdateDiffPositionService, feature_category: :code_review_workflow do
let(:project) { create(:project, :repository) }
let(:current_user) { project.first_owner }
let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
diff --git a/spec/services/draft_notes/create_service_spec.rb b/spec/services/draft_notes/create_service_spec.rb
index 528c8717525..93731a80dcc 100644
--- a/spec/services/draft_notes/create_service_spec.rb
+++ b/spec/services/draft_notes/create_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DraftNotes::CreateService do
+RSpec.describe DraftNotes::CreateService, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
let(:user) { merge_request.author }
diff --git a/spec/services/draft_notes/destroy_service_spec.rb b/spec/services/draft_notes/destroy_service_spec.rb
index 1f246a56eb3..f4cc9daa9e9 100644
--- a/spec/services/draft_notes/destroy_service_spec.rb
+++ b/spec/services/draft_notes/destroy_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DraftNotes::DestroyService do
+RSpec.describe DraftNotes::DestroyService, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
let(:user) { merge_request.author }
diff --git a/spec/services/draft_notes/publish_service_spec.rb b/spec/services/draft_notes/publish_service_spec.rb
index 44fe9063ac9..a4b1d8742d0 100644
--- a/spec/services/draft_notes/publish_service_spec.rb
+++ b/spec/services/draft_notes/publish_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe DraftNotes::PublishService do
+RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workflow do
include RepoHelpers
let(:merge_request) { create(:merge_request) }
diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb
index e8d3c0d673b..43fca75a5ea 100644
--- a/spec/services/emails/confirm_service_spec.rb
+++ b/spec/services/emails/confirm_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Emails::ConfirmService do
+RSpec.describe Emails::ConfirmService, feature_category: :user_management do
let_it_be(:user) { create(:user) }
subject(:service) { described_class.new(user) }
diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb
index b13197f21b8..3ef67036483 100644
--- a/spec/services/emails/create_service_spec.rb
+++ b/spec/services/emails/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Emails::CreateService do
+RSpec.describe Emails::CreateService, feature_category: :user_management do
let_it_be(:user) { create(:user) }
let(:opts) { { email: 'new@email.com', user: user } }
diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb
index 7dcf367016e..9d5e2b45647 100644
--- a/spec/services/emails/destroy_service_spec.rb
+++ b/spec/services/emails/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Emails::DestroyService do
+RSpec.describe Emails::DestroyService, feature_category: :user_management do
let!(:user) { create(:user) }
let!(:email) { create(:email, user: user) }
diff --git a/spec/services/environments/auto_stop_service_spec.rb b/spec/services/environments/auto_stop_service_spec.rb
index d688690c376..57fb860a557 100644
--- a/spec/services/environments/auto_stop_service_spec.rb
+++ b/spec/services/environments/auto_stop_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Environments::AutoStopService, :clean_gitlab_redis_shared_state, :sidekiq_inline do
+RSpec.describe Environments::AutoStopService, :clean_gitlab_redis_shared_state, :sidekiq_inline,
+ feature_category: :continuous_delivery do
include CreateEnvironmentsHelpers
include ExclusiveLeaseHelpers
diff --git a/spec/services/environments/canary_ingress/update_service_spec.rb b/spec/services/environments/canary_ingress/update_service_spec.rb
index 531f7d68a9f..f7d446c13f9 100644
--- a/spec/services/environments/canary_ingress/update_service_spec.rb
+++ b/spec/services/environments/canary_ingress/update_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_cache do
+RSpec.describe Environments::CanaryIngress::UpdateService, :clean_gitlab_redis_cache,
+ feature_category: :continuous_delivery do
include KubernetesHelpers
let_it_be(:project, refind: true) { create(:project) }
diff --git a/spec/services/environments/create_for_build_service_spec.rb b/spec/services/environments/create_for_build_service_spec.rb
index c7aadb20c01..223401a243d 100644
--- a/spec/services/environments/create_for_build_service_spec.rb
+++ b/spec/services/environments/create_for_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::CreateForBuildService do
+RSpec.describe Environments::CreateForBuildService, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/environments/reset_auto_stop_service_spec.rb b/spec/services/environments/reset_auto_stop_service_spec.rb
index 4a0b091c12d..a3b8b2e0aa1 100644
--- a/spec/services/environments/reset_auto_stop_service_spec.rb
+++ b/spec/services/environments/reset_auto_stop_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::ResetAutoStopService do
+RSpec.describe Environments::ResetAutoStopService, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
diff --git a/spec/services/environments/schedule_to_delete_review_apps_service_spec.rb b/spec/services/environments/schedule_to_delete_review_apps_service_spec.rb
index 401d6203b2c..3047f415815 100644
--- a/spec/services/environments/schedule_to_delete_review_apps_service_spec.rb
+++ b/spec/services/environments/schedule_to_delete_review_apps_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Environments::ScheduleToDeleteReviewAppsService do
+RSpec.describe Environments::ScheduleToDeleteReviewAppsService, feature_category: :continuous_delivery do
include ExclusiveLeaseHelpers
let_it_be(:maintainer) { create(:user) }
diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb
index 5f983a2151a..6e3b36b5636 100644
--- a/spec/services/environments/stop_service_spec.rb
+++ b/spec/services/environments/stop_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::StopService do
+RSpec.describe Environments::StopService, feature_category: :continuous_delivery do
include CreateEnvironmentsHelpers
let(:project) { create(:project, :private, :repository) }
diff --git a/spec/services/error_tracking/base_service_spec.rb b/spec/services/error_tracking/base_service_spec.rb
index de3523cb847..ed9efd9f95a 100644
--- a/spec/services/error_tracking/base_service_spec.rb
+++ b/spec/services/error_tracking/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::BaseService do
+RSpec.describe ErrorTracking::BaseService, feature_category: :error_tracking do
describe '#compose_response' do
let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user, id: non_existing_record_id) }
diff --git a/spec/services/error_tracking/collect_error_service_spec.rb b/spec/services/error_tracking/collect_error_service_spec.rb
index 159c070c683..3ff753e8c65 100644
--- a/spec/services/error_tracking/collect_error_service_spec.rb
+++ b/spec/services/error_tracking/collect_error_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::CollectErrorService do
+RSpec.describe ErrorTracking::CollectErrorService, feature_category: :error_tracking do
let_it_be(:project) { create(:project) }
let(:parsed_event_file) { 'error_tracking/parsed_event.json' }
diff --git a/spec/services/error_tracking/issue_details_service_spec.rb b/spec/services/error_tracking/issue_details_service_spec.rb
index 29f8154a27c..7ac41ffead6 100644
--- a/spec/services/error_tracking/issue_details_service_spec.rb
+++ b/spec/services/error_tracking/issue_details_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::IssueDetailsService do
+RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
include_context 'sentry error tracking context'
subject { described_class.new(project, user, params) }
diff --git a/spec/services/error_tracking/issue_latest_event_service_spec.rb b/spec/services/error_tracking/issue_latest_event_service_spec.rb
index aa2430ddffb..bfde14c7ef1 100644
--- a/spec/services/error_tracking/issue_latest_event_service_spec.rb
+++ b/spec/services/error_tracking/issue_latest_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::IssueLatestEventService do
+RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_tracking do
include_context 'sentry error tracking context'
let(:params) { {} }
diff --git a/spec/services/error_tracking/issue_update_service_spec.rb b/spec/services/error_tracking/issue_update_service_spec.rb
index a06c3588264..4dae6cc2fa0 100644
--- a/spec/services/error_tracking/issue_update_service_spec.rb
+++ b/spec/services/error_tracking/issue_update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::IssueUpdateService do
+RSpec.describe ErrorTracking::IssueUpdateService, feature_category: :error_tracking do
include_context 'sentry error tracking context'
let(:arguments) { { issue_id: non_existing_record_id, status: 'resolved' } }
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index a7bd6c75df5..2c35c2b8acd 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTracking::ListIssuesService do
+RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracking do
include_context 'sentry error tracking context'
let(:params) { {} }
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index b969bd76053..d14f130233e 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state do
+RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state, feature_category: :service_ping do
include SnowplowHelpers
let(:service) { described_class.new }
@@ -12,10 +12,20 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
shared_examples 'it records the event in the event counter' do
specify do
- tracking_params = { event_action: event_action, date_from: Date.yesterday, date_to: Date.today }
+ tracking_params = { event_names: event_action, start_date: Date.yesterday, end_date: Date.today }
expect { subject }
- .to change { Gitlab::UsageDataCounters::TrackUniqueEvents.count_unique_events(**tracking_params) }
+ .to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(**tracking_params) }
+ .by(1)
+ end
+ end
+
+ shared_examples 'it records a git write event' do
+ specify do
+ tracking_params = { event_names: 'git_write_action', start_date: Date.yesterday, end_date: Date.today }
+
+ expect { subject }
+ .to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(**tracking_params) }
.by(1)
end
end
@@ -65,11 +75,10 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
+ let(:event_action) { :merge_request_action }
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'created' }
let(:label) { described_class::MR_EVENT_LABEL }
@@ -95,11 +104,10 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
+ let(:event_action) { :merge_request_action }
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'closed' }
let(:label) { described_class::MR_EVENT_LABEL }
@@ -125,11 +133,10 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
+ let(:event_action) { :merge_request_action }
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'merged' }
let(:label) { described_class::MR_EVENT_LABEL }
@@ -276,8 +283,10 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION }
+ let(:event_action) { :wiki_action }
end
+
+ it_behaves_like "it records a git write event"
end
(Event.actions.keys - Event::WIKI_ACTIONS).each do |bad_action|
@@ -312,9 +321,11 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
it_behaves_like 'service for creating a push event', PushEventPayloadService
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
+ let(:event_action) { :project_action }
end
+ it_behaves_like "it records a git write event"
+
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:category) { described_class.to_s }
let(:action) { :push }
@@ -338,9 +349,11 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
+ let(:event_action) { :project_action }
end
+ it_behaves_like "it records a git write event"
+
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:category) { described_class.to_s }
let(:action) { :push }
@@ -400,16 +413,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
+ let(:event_action) { :design_action }
end
+ it_behaves_like "it records a git write event"
+
describe 'Snowplow tracking' do
let(:project) { design.project }
let(:namespace) { project.namespace }
let(:category) { described_class.name }
- let(:property) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s }
+ let(:property) { :design_action.to_s }
let(:label) { ::EventCreateService::DEGIGN_EVENT_LABEL }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
context 'for create event' do
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
@@ -448,9 +462,11 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
it_behaves_like "it records the event in the event counter" do
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
+ let(:event_action) { :design_action }
end
+ it_behaves_like "it records a git write event"
+
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject(:design_service) { service.destroy_designs([design], author) }
@@ -459,9 +475,8 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:category) { described_class.name }
let(:action) { 'destroy' }
let(:user) { author }
- let(:property) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION.to_s }
+ let(:property) { :design_action.to_s }
let(:label) { ::EventCreateService::DEGIGN_EVENT_LABEL }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
end
@@ -471,7 +486,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:note) { create(:note) }
let(:author) { create(:user) }
- let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
+ let(:event_action) { :merge_request_action }
it { expect(leave_note).to be_truthy }
@@ -485,7 +500,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
it_behaves_like "it records the event in the event counter"
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:note) { create(:diff_note_on_merge_request) }
let(:category) { described_class.name }
let(:action) { 'commented' }
@@ -502,10 +516,9 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
context 'when it is not a diff note' do
it 'does not change the unique action counter' do
- counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
- tracking_params = { event_action: event_action, date_from: Date.yesterday, date_to: Date.today }
+ tracking_params = { event_names: event_action, start_date: Date.yesterday, end_date: Date.today }
- expect { subject }.not_to change { counter_class.count_unique_events(**tracking_params) }
+ expect { subject }.not_to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(**tracking_params) }
end
end
end
diff --git a/spec/services/events/destroy_service_spec.rb b/spec/services/events/destroy_service_spec.rb
index 8b07852c040..e50fe247238 100644
--- a/spec/services/events/destroy_service_spec.rb
+++ b/spec/services/events/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Events::DestroyService do
+RSpec.describe Events::DestroyService, feature_category: :user_profile do
subject(:service) { described_class.new(project) }
let_it_be(:project, reload: true) { create(:project, :repository) }
diff --git a/spec/services/events/render_service_spec.rb b/spec/services/events/render_service_spec.rb
index 24a3b9abe14..2e8cd26781b 100644
--- a/spec/services/events/render_service_spec.rb
+++ b/spec/services/events/render_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Events::RenderService do
+RSpec.describe Events::RenderService, feature_category: :user_profile do
describe '#execute' do
let!(:note) { build(:note) }
let!(:event) { build(:event, target: note, project: note.project) }
diff --git a/spec/services/feature_flags/create_service_spec.rb b/spec/services/feature_flags/create_service_spec.rb
index 1a32faad948..18c48714ccd 100644
--- a/spec/services/feature_flags/create_service_spec.rb
+++ b/spec/services/feature_flags/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FeatureFlags::CreateService do
+RSpec.describe FeatureFlags::CreateService, feature_category: :feature_flags do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
@@ -46,6 +46,8 @@ RSpec.describe FeatureFlags::CreateService do
end
context 'when feature flag is saved correctly' do
+ let(:audit_event_details) { AuditEvent.last.details }
+ let(:audit_event_message) { audit_event_details[:custom_message] }
let(:params) do
{
name: 'feature_flag',
@@ -88,9 +90,9 @@ RSpec.describe FeatureFlags::CreateService do
it 'creates audit event', :with_license do
expect { subject }.to change { AuditEvent.count }.by(1)
- expect(AuditEvent.last.details[:custom_message]).to start_with('Created feature flag feature_flag with description "description".')
- expect(AuditEvent.last.details[:custom_message]).to include('Created strategy "default" with scopes "*".')
- expect(AuditEvent.last.details[:custom_message]).to include('Created strategy "default" with scopes "production".')
+ expect(audit_event_message).to start_with('Created feature flag feature_flag with description "description".')
+ expect(audit_event_message).to include('Created strategy "default" with scopes "*".')
+ expect(audit_event_message).to include('Created strategy "default" with scopes "production".')
end
context 'when user is reporter' do
diff --git a/spec/services/feature_flags/destroy_service_spec.rb b/spec/services/feature_flags/destroy_service_spec.rb
index b2793dc0560..1ec0ee6e68c 100644
--- a/spec/services/feature_flags/destroy_service_spec.rb
+++ b/spec/services/feature_flags/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FeatureFlags::DestroyService do
+RSpec.describe FeatureFlags::DestroyService, feature_category: :feature_flags do
include FeatureFlagHelpers
let_it_be(:project) { create(:project) }
@@ -20,7 +20,8 @@ RSpec.describe FeatureFlags::DestroyService do
describe '#execute' do
subject { described_class.new(project, user, params).execute(feature_flag) }
- let(:audit_event_message) { AuditEvent.last.details[:custom_message] }
+ let(:audit_event_details) { AuditEvent.last.details }
+ let(:audit_event_message) { audit_event_details[:custom_message] }
let(:params) { {} }
it 'returns status success' do
diff --git a/spec/services/feature_flags/hook_service_spec.rb b/spec/services/feature_flags/hook_service_spec.rb
index f3edaca52a9..2a3ce9085b8 100644
--- a/spec/services/feature_flags/hook_service_spec.rb
+++ b/spec/services/feature_flags/hook_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FeatureFlags::HookService do
+RSpec.describe FeatureFlags::HookService, feature_category: :feature_flags do
describe '#execute_hooks' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index 1c5af71a50a..55c09f06f16 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FeatureFlags::UpdateService, :with_license do
+RSpec.describe FeatureFlags::UpdateService, :with_license, feature_category: :feature_flags do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
@@ -19,9 +19,8 @@ RSpec.describe FeatureFlags::UpdateService, :with_license do
subject { described_class.new(project, user, params).execute(feature_flag) }
let(:params) { { name: 'new_name' } }
- let(:audit_event_message) do
- AuditEvent.last.details[:custom_message]
- end
+ let(:audit_event_details) { AuditEvent.last.details }
+ let(:audit_event_message) { audit_event_details[:custom_message] }
it 'returns success status' do
expect(subject[:status]).to eq(:success)
diff --git a/spec/services/files/create_service_spec.rb b/spec/services/files/create_service_spec.rb
index 3b3dbd1fcfe..26f57f43120 100644
--- a/spec/services/files/create_service_spec.rb
+++ b/spec/services/files/create_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Files::CreateService do
+RSpec.describe Files::CreateService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user, :commit_email) }
diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb
index 3823d027812..dd99e5f9742 100644
--- a/spec/services/files/delete_service_spec.rb
+++ b/spec/services/files/delete_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Files::DeleteService do
+RSpec.describe Files::DeleteService, feature_category: :source_code_management do
subject { described_class.new(project, user, commit_params) }
let(:project) { create(:project, :repository) }
@@ -52,8 +52,8 @@ RSpec.describe Files::DeleteService do
end
describe "#execute" do
- context "when the file's last commit sha does not match the supplied last_commit_sha" do
- let(:last_commit_sha) { "foo" }
+ context "when the file's last commit is earlier than the latest commit for this branch" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).parent_id }
it "returns a hash with the correct error message and a :error status" do
expect { subject.execute }
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
index 6a5c7d2749d..7149fa77d6a 100644
--- a/spec/services/files/multi_service_spec.rb
+++ b/spec/services/files/multi_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Files::MultiService do
+RSpec.describe Files::MultiService, feature_category: :source_code_management do
subject { described_class.new(project, user, commit_params) }
let(:project) { create(:project, :repository) }
@@ -19,6 +19,10 @@ RSpec.describe Files::MultiService do
Gitlab::Git::Commit.last_for_path(project.repository, branch_name, original_file_path).sha
end
+ let(:branch_commit_id) do
+ Gitlab::Git::Commit.find(project.repository, branch_name).sha
+ end
+
let(:default_action) do
{
action: action,
@@ -78,6 +82,16 @@ RSpec.describe Files::MultiService do
end
end
+ context 'when file not changed, but later commit id is used' do
+ let(:actions) { [default_action.merge(last_commit_id: branch_commit_id)] }
+
+ it 'accepts the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+
context 'when the file have not been modified' do
it 'accepts the commit' do
results = subject.execute
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index 6d7459e0b29..6a9f9d6b86f 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Files::UpdateService do
+RSpec.describe Files::UpdateService, feature_category: :source_code_management do
subject { described_class.new(project, user, commit_params) }
let(:project) { create(:project, :repository) }
@@ -31,8 +31,8 @@ RSpec.describe Files::UpdateService do
end
describe "#execute" do
- context "when the file's last commit sha does not match the supplied last_commit_sha" do
- let(:last_commit_sha) { "foo" }
+ context "when the file's last commit sha is earlier than the latest change for that branch" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).parent_id }
it "returns a hash with the correct error message and a :error status" do
expect { subject.execute }
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index 5afd7b30ab0..9d49943ccfb 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::BaseHooksService do
+RSpec.describe Git::BaseHooksService, feature_category: :source_code_management do
include RepoHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 973ead28462..e991b5bd842 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state do
+RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
include RepoHelpers
include ProjectForksHelper
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index a9f5b07fef4..aa534777f3e 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, :use_clean_rails_redis_caching, services: true do
+RSpec.describe Git::BranchPushService, :use_clean_rails_redis_caching, services: true, feature_category: :source_code_management do
include RepoHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index 8d2da4a899e..9ec13bc957b 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::ProcessRefChangesService do
+RSpec.describe Git::ProcessRefChangesService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:params) { { changes: git_changes } }
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
index 01a0d2e8600..73f6eff36ba 100644
--- a/spec/services/git/tag_hooks_service_spec.rb
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::TagHooksService, :service do
+RSpec.describe Git::TagHooksService, :service, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index 597254d46fa..0d40c331d11 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::TagPushService do
+RSpec.describe Git::TagPushService, feature_category: :source_code_management do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/services/git/wiki_push_service/change_spec.rb b/spec/services/git/wiki_push_service/change_spec.rb
index 3616bf62b20..ad3c4ae68c0 100644
--- a/spec/services/git/wiki_push_service/change_spec.rb
+++ b/spec/services/git/wiki_push_service/change_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::WikiPushService::Change do
+RSpec.describe Git::WikiPushService::Change, feature_category: :source_code_management do
subject { described_class.new(project_wiki, change, raw_change) }
let(:project_wiki) { double('ProjectWiki') }
diff --git a/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb b/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb
index cd0dd75e576..4f2e0bea623 100644
--- a/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb
+++ b/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::CreateCloudsqlInstanceService do
+RSpec.describe GoogleCloud::CreateCloudsqlInstanceService, feature_category: :deployment_management do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:gcp_project_id) { 'gcp_project_120' }
diff --git a/spec/services/google_cloud/create_service_accounts_service_spec.rb b/spec/services/google_cloud/create_service_accounts_service_spec.rb
index 3f500e7c235..3b57f2a9e5f 100644
--- a/spec/services/google_cloud/create_service_accounts_service_spec.rb
+++ b/spec/services/google_cloud/create_service_accounts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::CreateServiceAccountsService do
+RSpec.describe GoogleCloud::CreateServiceAccountsService, feature_category: :deployment_management do
describe '#execute' do
before do
mock_google_oauth2_creds = Struct.new(:app_id, :app_secret)
diff --git a/spec/services/google_cloud/enable_cloud_run_service_spec.rb b/spec/services/google_cloud/enable_cloud_run_service_spec.rb
index 6d2b1f5cfd5..3de9e7fcd5c 100644
--- a/spec/services/google_cloud/enable_cloud_run_service_spec.rb
+++ b/spec/services/google_cloud/enable_cloud_run_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::EnableCloudRunService do
+RSpec.describe GoogleCloud::EnableCloudRunService, feature_category: :deployment_management do
describe 'when a project does not have any gcp projects' do
let_it_be(:project) { create(:project) }
diff --git a/spec/services/google_cloud/enable_cloudsql_service_spec.rb b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
index aa6d2402d7c..b14b827e8b8 100644
--- a/spec/services/google_cloud/enable_cloudsql_service_spec.rb
+++ b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::EnableCloudsqlService do
+RSpec.describe GoogleCloud::EnableCloudsqlService, feature_category: :deployment_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:params) do
diff --git a/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb b/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb
index b2cd5632be0..a748fed7134 100644
--- a/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb
+++ b/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::GcpRegionAddOrReplaceService do
+RSpec.describe GoogleCloud::GcpRegionAddOrReplaceService, feature_category: :deployment_management do
it 'adds and replaces GCP region vars' do
project = create(:project, :public)
service = described_class.new(project)
diff --git a/spec/services/google_cloud/generate_pipeline_service_spec.rb b/spec/services/google_cloud/generate_pipeline_service_spec.rb
index a78d8ff6661..c18514884ca 100644
--- a/spec/services/google_cloud/generate_pipeline_service_spec.rb
+++ b/spec/services/google_cloud/generate_pipeline_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::GeneratePipelineService do
+RSpec.describe GoogleCloud::GeneratePipelineService, feature_category: :deployment_management do
describe 'for cloud-run' do
describe 'when there is no existing pipeline' do
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb b/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb
index 4587a5077c0..ed41d0fd487 100644
--- a/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb
+++ b/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::GetCloudsqlInstancesService do
+RSpec.describe GoogleCloud::GetCloudsqlInstancesService, feature_category: :deployment_management do
let(:service) { described_class.new(project) }
let(:project) { create(:project) }
diff --git a/spec/services/google_cloud/service_accounts_service_spec.rb b/spec/services/google_cloud/service_accounts_service_spec.rb
index 10e387126a3..c900bf7d300 100644
--- a/spec/services/google_cloud/service_accounts_service_spec.rb
+++ b/spec/services/google_cloud/service_accounts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::ServiceAccountsService do
+RSpec.describe GoogleCloud::ServiceAccountsService, feature_category: :deployment_management do
let(:service) { described_class.new(project) }
describe 'find_for_project' do
diff --git a/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb b/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb
index 0a0f05ab4be..5095277f61a 100644
--- a/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb
+++ b/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do
+RSpec.describe GoogleCloud::SetupCloudsqlInstanceService, feature_category: :deployment_management do
let(:random_user) { create(:user) }
let(:project) { create(:project) }
let(:list_databases_empty) { Google::Apis::SqladminV1beta4::ListDatabasesResponse.new(items: []) }
diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb
index 9ac56355b4b..d603ce951ec 100644
--- a/spec/services/gpg_keys/create_service_spec.rb
+++ b/spec/services/gpg_keys/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GpgKeys::CreateService do
+RSpec.describe GpgKeys::CreateService, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:params) { attributes_for(:gpg_key) }
diff --git a/spec/services/grafana/proxy_service_spec.rb b/spec/services/grafana/proxy_service_spec.rb
index 99120de3593..7029bab379a 100644
--- a/spec/services/grafana/proxy_service_spec.rb
+++ b/spec/services/grafana/proxy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Grafana::ProxyService do
+RSpec.describe Grafana::ProxyService, feature_category: :metrics do
include ReactiveCachingHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/services/gravatar_service_spec.rb b/spec/services/gravatar_service_spec.rb
index a6418b02f78..6ccb362cc5c 100644
--- a/spec/services/gravatar_service_spec.rb
+++ b/spec/services/gravatar_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GravatarService do
+RSpec.describe GravatarService, feature_category: :user_profile do
describe '#execute' do
let(:url) { 'http://example.com/avatar?hash=%{hash}&size=%{size}&email=%{email}&username=%{username}' }
diff --git a/spec/services/groups/auto_devops_service_spec.rb b/spec/services/groups/auto_devops_service_spec.rb
index 486a99dd8df..0724e072dab 100644
--- a/spec/services/groups/auto_devops_service_spec.rb
+++ b/spec/services/groups/auto_devops_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Groups::AutoDevopsService, '#execute' do
+RSpec.describe Groups::AutoDevopsService, '#execute', feature_category: :auto_devops do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/groups/autocomplete_service_spec.rb b/spec/services/groups/autocomplete_service_spec.rb
index 00d0ad3b347..9f55322e72d 100644
--- a/spec/services/groups/autocomplete_service_spec.rb
+++ b/spec/services/groups/autocomplete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::AutocompleteService do
+RSpec.describe Groups::AutocompleteService, feature_category: :subgroups do
let_it_be(:group, refind: true) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
let_it_be(:sub_group) { create(:group, :private, parent: group) }
diff --git a/spec/services/groups/deploy_tokens/create_service_spec.rb b/spec/services/groups/deploy_tokens/create_service_spec.rb
index 0c28075f998..e408c2787d8 100644
--- a/spec/services/groups/deploy_tokens/create_service_spec.rb
+++ b/spec/services/groups/deploy_tokens/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::DeployTokens::CreateService do
+RSpec.describe Groups::DeployTokens::CreateService, feature_category: :deployment_management do
it_behaves_like 'a deploy token creation service' do
let(:entity) { create(:group) }
let(:deploy_token_class) { GroupDeployToken }
diff --git a/spec/services/groups/deploy_tokens/destroy_service_spec.rb b/spec/services/groups/deploy_tokens/destroy_service_spec.rb
index 28e60b12993..c4694758b2f 100644
--- a/spec/services/groups/deploy_tokens/destroy_service_spec.rb
+++ b/spec/services/groups/deploy_tokens/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::DeployTokens::DestroyService do
+RSpec.describe Groups::DeployTokens::DestroyService, feature_category: :deployment_management do
it_behaves_like 'a deploy token deletion service' do
let_it_be(:entity) { create(:group) }
let_it_be(:deploy_token_class) { GroupDeployToken }
diff --git a/spec/services/groups/deploy_tokens/revoke_service_spec.rb b/spec/services/groups/deploy_tokens/revoke_service_spec.rb
index fcf11bbb8e6..c302dd14e3b 100644
--- a/spec/services/groups/deploy_tokens/revoke_service_spec.rb
+++ b/spec/services/groups/deploy_tokens/revoke_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::DeployTokens::RevokeService do
+RSpec.describe Groups::DeployTokens::RevokeService, feature_category: :deployment_management do
let_it_be(:entity) { create(:group) }
let_it_be(:deploy_token) { create(:deploy_token, :group, groups: [entity]) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/groups/group_links/create_service_spec.rb b/spec/services/groups/group_links/create_service_spec.rb
index bfbaedbd06f..ced87421858 100644
--- a/spec/services/groups/group_links/create_service_spec.rb
+++ b/spec/services/groups/group_links/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
+RSpec.describe Groups::GroupLinks::CreateService, '#execute', feature_category: :subgroups do
let_it_be(:shared_with_group_parent) { create(:group, :private) }
let_it_be(:shared_with_group) { create(:group, :private, parent: shared_with_group_parent) }
let_it_be(:shared_with_group_child) { create(:group, :private, parent: shared_with_group) }
diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb
index a570c28cf8b..5821ec44192 100644
--- a/spec/services/groups/group_links/destroy_service_spec.rb
+++ b/spec/services/groups/group_links/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::GroupLinks::DestroyService, '#execute' do
+RSpec.describe Groups::GroupLinks::DestroyService, '#execute', feature_category: :subgroups do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private) }
diff --git a/spec/services/groups/group_links/update_service_spec.rb b/spec/services/groups/group_links/update_service_spec.rb
index 31446c8e4bf..f17d2f50a02 100644
--- a/spec/services/groups/group_links/update_service_spec.rb
+++ b/spec/services/groups/group_links/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::GroupLinks::UpdateService, '#execute' do
+RSpec.describe Groups::GroupLinks::UpdateService, '#execute', feature_category: :subgroups do
let(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
@@ -18,7 +18,7 @@ RSpec.describe Groups::GroupLinks::UpdateService, '#execute' do
expires_at: expiry_date }
end
- subject { described_class.new(link).execute(group_link_params) }
+ subject { described_class.new(link, user).execute(group_link_params) }
before do
group.add_developer(group_member_user)
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index ec42a728409..c44c2e3b911 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::ImportExport::ExportService do
+RSpec.describe Groups::ImportExport::ExportService, feature_category: :importers do
describe '#async_execute' do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index 972b12d7ee5..75db6e26cbf 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::ImportExport::ImportService do
+RSpec.describe Groups::ImportExport::ImportService, feature_category: :importers do
describe '#async_execute' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/groups/merge_requests_count_service_spec.rb b/spec/services/groups/merge_requests_count_service_spec.rb
index 8bd350d6f0e..32c4c618eda 100644
--- a/spec/services/groups/merge_requests_count_service_spec.rb
+++ b/spec/services/groups/merge_requests_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::MergeRequestsCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Groups::MergeRequestsCountService, :use_clean_rails_memory_store_caching, feature_category: :subgroups do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/services/groups/nested_create_service_spec.rb b/spec/services/groups/nested_create_service_spec.rb
index a43c1d8d9c3..476bc2aa23c 100644
--- a/spec/services/groups/nested_create_service_spec.rb
+++ b/spec/services/groups/nested_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::NestedCreateService do
+RSpec.describe Groups::NestedCreateService, feature_category: :subgroups do
let(:user) { create(:user) }
subject(:service) { described_class.new(user, params) }
diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb
index 923caa6c150..725b913bf15 100644
--- a/spec/services/groups/open_issues_count_service_spec.rb
+++ b/spec/services/groups/open_issues_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_caching, feature_category: :subgroups do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/groups/participants_service_spec.rb b/spec/services/groups/participants_service_spec.rb
index 750aead277f..37966a523c2 100644
--- a/spec/services/groups/participants_service_spec.rb
+++ b/spec/services/groups/participants_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::ParticipantsService do
+RSpec.describe Groups::ParticipantsService, feature_category: :subgroups do
describe '#group_members' do
let(:user) { create(:user) }
let(:parent_group) { create(:group) }
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index c758d3d5477..6baa8e5d6b6 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UpdateService do
+RSpec.describe Groups::UpdateService, feature_category: :subgroups do
let!(:user) { create(:user) }
let!(:private_group) { create(:group, :private) }
let!(:internal_group) { create(:group, :internal) }
diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb
index a29f73a71c2..48c81f109aa 100644
--- a/spec/services/groups/update_shared_runners_service_spec.rb
+++ b/spec/services/groups/update_shared_runners_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UpdateSharedRunnersService do
+RSpec.describe Groups::UpdateSharedRunnersService, feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:params) { {} }
diff --git a/spec/services/groups/update_statistics_service_spec.rb b/spec/services/groups/update_statistics_service_spec.rb
index 84b18b670a7..13a88839de0 100644
--- a/spec/services/groups/update_statistics_service_spec.rb
+++ b/spec/services/groups/update_statistics_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UpdateStatisticsService do
+RSpec.describe Groups::UpdateStatisticsService, feature_category: :subgroups do
let_it_be(:group, reload: true) { create(:group) }
let(:statistics) { %w(wiki_size) }
diff --git a/spec/services/ide/base_config_service_spec.rb b/spec/services/ide/base_config_service_spec.rb
index ee57f2c18ec..ac57a13d7fc 100644
--- a/spec/services/ide/base_config_service_spec.rb
+++ b/spec/services/ide/base_config_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ide::BaseConfigService do
+RSpec.describe Ide::BaseConfigService, feature_category: :web_ide do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ide/schemas_config_service_spec.rb b/spec/services/ide/schemas_config_service_spec.rb
index f277b8e9954..b6f229edc78 100644
--- a/spec/services/ide/schemas_config_service_spec.rb
+++ b/spec/services/ide/schemas_config_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ide::SchemasConfigService do
+RSpec.describe Ide::SchemasConfigService, feature_category: :web_ide do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ide/terminal_config_service_spec.rb b/spec/services/ide/terminal_config_service_spec.rb
index 73614f28b06..76c8d9f2e6f 100644
--- a/spec/services/ide/terminal_config_service_spec.rb
+++ b/spec/services/ide/terminal_config_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ide::TerminalConfigService do
+RSpec.describe Ide::TerminalConfigService, feature_category: :web_ide do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb
index 555812ca9cf..aea6c45b3a8 100644
--- a/spec/services/import/bitbucket_server_service_spec.rb
+++ b/spec/services/import/bitbucket_server_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::BitbucketServerService do
+RSpec.describe Import::BitbucketServerService, feature_category: :importers do
let_it_be(:user) { create(:user) }
let(:base_uri) { "https://test:7990" }
diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb
index 027d0240a7a..6953213add7 100644
--- a/spec/services/import/fogbugz_service_spec.rb
+++ b/spec/services/import/fogbugz_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::FogbugzService do
+RSpec.describe Import::FogbugzService, feature_category: :importers do
let_it_be(:user) { create(:user) }
let(:base_uri) { "https://test:7990" }
diff --git a/spec/services/import/github/cancel_project_import_service_spec.rb b/spec/services/import/github/cancel_project_import_service_spec.rb
index 77b8771ee65..d8ea303fa50 100644
--- a/spec/services/import/github/cancel_project_import_service_spec.rb
+++ b/spec/services/import/github/cancel_project_import_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::Github::CancelProjectImportService do
+RSpec.describe Import::Github::CancelProjectImportService, feature_category: :importers do
subject(:import_cancel) { described_class.new(project, project.owner) }
let_it_be(:user) { create(:user) }
@@ -14,6 +14,18 @@ RSpec.describe Import::Github::CancelProjectImportService do
it 'update import state to be canceled' do
expect(import_cancel.execute).to eq({ status: :success, project: project })
end
+
+ it 'tracks canceled imports' do
+ metrics_double = instance_double('Gitlab::Import::Metrics')
+
+ expect(Gitlab::Import::Metrics)
+ .to receive(:new)
+ .with(:github_importer, project)
+ .and_return(metrics_double)
+ expect(metrics_double).to receive(:track_canceled_import)
+
+ import_cancel.execute
+ end
end
context 'when import is finished' do
diff --git a/spec/services/import/github/notes/create_service_spec.rb b/spec/services/import/github/notes/create_service_spec.rb
index 57699def848..37cb903b66e 100644
--- a/spec/services/import/github/notes/create_service_spec.rb
+++ b/spec/services/import/github/notes/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::Github::Notes::CreateService do
+RSpec.describe Import::Github::Notes::CreateService, feature_category: :importers do
it 'does not support quick actions' do
project = create(:project, :repository)
user = create(:user)
diff --git a/spec/services/import/github_service_spec.rb b/spec/services/import/github_service_spec.rb
index 293e247c140..5d762568a62 100644
--- a/spec/services/import/github_service_spec.rb
+++ b/spec/services/import/github_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GithubService do
+RSpec.describe Import::GithubService, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:token) { 'complex-token' }
let_it_be(:access_params) { { github_access_token: 'github-complex-token' } }
diff --git a/spec/services/import/gitlab_projects/create_project_service_spec.rb b/spec/services/import/gitlab_projects/create_project_service_spec.rb
index 59c384bad3c..35378bcee92 100644
--- a/spec/services/import/gitlab_projects/create_project_service_spec.rb
+++ b/spec/services/import/gitlab_projects/create_project_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Import::GitlabProjects::CreateProjectService, :aggregate_failures do
+RSpec.describe ::Import::GitlabProjects::CreateProjectService, :aggregate_failures, feature_category: :importers do
let(:fake_file_acquisition_strategy) do
Class.new do
attr_reader :errors
@@ -127,7 +127,7 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectService, :aggregate_failur
expect(response.payload).to eq(other_errors: [])
end
- context 'when the project contains multilple errors' do
+ context 'when the project contains multiple errors' do
it 'fails to create a project' do
params.merge!(name: '_ an invalid name _', path: '_ an invalid path _')
@@ -137,10 +137,13 @@ RSpec.describe ::Import::GitlabProjects::CreateProjectService, :aggregate_failur
expect(response).to be_error
expect(response.http_status).to eq(:bad_request)
- expect(response.message)
- .to eq(%{Project namespace path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'})
+ expect(response.message).to eq(
+ 'Project namespace path must not start or end with a special character and must not contain consecutive ' \
+ 'special characters.'
+ )
expect(response.payload).to eq(
other_errors: [
+ %{Project namespace path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'},
%{Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'},
%{Path must not start or end with a special character and must not contain consecutive special characters.}
])
diff --git a/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
index 3c788138157..3a94ed02dd5 100644
--- a/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
+++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::FileUpload, :aggregate_failures do
+RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::FileUpload, :aggregate_failures, feature_category: :importers do
let(:file) { UploadedFile.new(File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz')) }
describe 'validation' do
diff --git a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb
index d9042e95149..411e2ec5286 100644
--- a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb
+++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_s3_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFileS3, :aggregate_failures do
+RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFileS3, :aggregate_failures, feature_category: :importers do
let(:region_name) { 'region_name' }
let(:bucket_name) { 'bucket_name' }
let(:file_key) { 'file_key' }
diff --git a/spec/services/import/prepare_service_spec.rb b/spec/services/import/prepare_service_spec.rb
index 0097198f7a9..fcb90575d96 100644
--- a/spec/services/import/prepare_service_spec.rb
+++ b/spec/services/import/prepare_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::PrepareService do
+RSpec.describe Import::PrepareService, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
index 221ac2cd73a..1d2b3975832 100644
--- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb
+++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::ValidateRemoteGitEndpointService do
+RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :importers do
include StubRequests
let_it_be(:base_url) { 'http://demo.host/path' }
@@ -35,6 +35,28 @@ RSpec.describe Import::ValidateRemoteGitEndpointService do
end
end
+ context 'when uri is using an invalid protocol' do
+ subject { described_class.new(url: 'ssh://demo.host/repo') }
+
+ it 'reports error when invalid URL is provided' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ end
+ end
+
+ context 'when uri is invalid' do
+ subject { described_class.new(url: 'http:example.com') }
+
+ it 'reports error when invalid URL is provided' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ end
+ end
+
context 'when receiving HTTP response' do
subject { described_class.new(url: base_url) }
diff --git a/spec/services/import_csv/base_service_spec.rb b/spec/services/import_csv/base_service_spec.rb
index 0c0ed40ff4d..93fff0d546a 100644
--- a/spec/services/import_csv/base_service_spec.rb
+++ b/spec/services/import_csv/base_service_spec.rb
@@ -24,40 +24,65 @@ RSpec.describe ImportCsv::BaseService, feature_category: :importers do
it_behaves_like 'abstract method', :validate_headers_presence!, "any"
it_behaves_like 'abstract method', :create_object_class
- describe '#detect_col_sep' do
- context 'when header contains invalid separators' do
- it 'raises error' do
- header = 'Name&email'
+ context 'when given a class' do
+ let(:importer_klass) do
+ Class.new(described_class) do
+ def attributes_for(row)
+ { title: row[:title] }
+ end
- expect { subject.send(:detect_col_sep, header) }.to raise_error(CSV::MalformedCSVError)
- end
- end
+ def validate_headers_presence!(headers)
+ raise CSV::MalformedCSVError.new("Missing required headers", 1) unless headers.present?
+ end
- context 'when header is valid' do
- shared_examples 'header with valid separators' do
- let(:header) { "Name#{separator}email" }
+ def create_object_class
+ Class.new
+ end
- it 'returns separator value' do
- expect(subject.send(:detect_col_sep, header)).to eq(separator)
+ def email_results_to_user
+ # no-op
end
end
+ end
- context 'with ; as separator' do
- let(:separator) { ';' }
+ let(:service) do
+ uploader = FileUploader.new(project)
+ uploader.store!(file)
- it_behaves_like 'header with valid separators'
- end
+ importer_klass.new(user, project, uploader)
+ end
+
+ subject { service.execute }
+
+ it_behaves_like 'correctly handles invalid files'
+
+ describe '#detect_col_sep' do
+ using RSpec::Parameterized::TableSyntax
- context 'with \t as separator' do
- let(:separator) { "\t" }
+ let(:file) { double }
- it_behaves_like 'header with valid separators'
+ before do
+ allow(service).to receive_message_chain('csv_data.lines.first').and_return(header)
end
- context 'with , as separator' do
- let(:separator) { ',' }
+ where(:sep_character, :valid) do
+ '&' | false
+ '?' | false
+ ';' | true
+ ',' | true
+ "\t" | true
+ end
+
+ with_them do
+ let(:header) { "Name#{sep_character}email" }
- it_behaves_like 'header with valid separators'
+ it 'responds appropriately' do
+ if valid
+ expect(service.send(:detect_col_sep)).to eq sep_character
+ else
+ expect { service.send(:detect_col_sep) }.to raise_error(CSV::MalformedCSVError)
+ end
+ end
end
end
end
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
index 2bcdfa6dd8f..7b638b4948b 100644
--- a/spec/services/import_export_clean_up_service_spec.rb
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ImportExportCleanUpService do
+RSpec.describe ImportExportCleanUpService, feature_category: :importers do
describe '#execute' do
let(:service) { described_class.new }
diff --git a/spec/services/incident_management/incidents/create_service_spec.rb b/spec/services/incident_management/incidents/create_service_spec.rb
index 7db762b9c5b..e6ded379434 100644
--- a/spec/services/incident_management/incidents/create_service_spec.rb
+++ b/spec/services/incident_management/incidents/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::Incidents::CreateService do
+RSpec.describe IncidentManagement::Incidents::CreateService, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { User.alert_bot }
diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
index 4b0c8d9113c..9b1994af1bb 100644
--- a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
+++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService,
+ feature_category: :incident_management do
let_it_be(:current_user) { create(:user) }
let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) }
let_it_be(:issue, reload: true) { escalation_status.issue }
diff --git a/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb
index b5c5238d483..56a159f452c 100644
--- a/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb
+++ b/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::IssuableEscalationStatuses::BuildService do
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::BuildService, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:incident, reload: true) { create(:incident, project: project) }
diff --git a/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb
index b6ae03a19fe..e6c63d63123 100644
--- a/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb
+++ b/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::IssuableEscalationStatuses::CreateService do
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::CreateService, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let(:incident) { create(:incident, project: project) }
diff --git a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
index e8208c410d5..3f3174d0112 100644
--- a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
+++ b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateService, factory_default: :keep do
+RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateService, factory_default: :keep,
+ feature_category: :incident_management do
let_it_be(:project) { create_default(:project) }
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, :triggered) }
let_it_be(:user_with_permissions) { create(:user) }
diff --git a/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb b/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
index 2fda789cf56..caa5ee495b7 100644
--- a/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
+++ b/spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::PagerDuty::CreateIncidentIssueService do
+RSpec.describe IncidentManagement::PagerDuty::CreateIncidentIssueService, feature_category: :incident_management do
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { User.alert_bot }
diff --git a/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb b/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
index e2aba0b61af..06f423bc63c 100644
--- a/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
+++ b/spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::PagerDuty::ProcessWebhookService do
+RSpec.describe IncidentManagement::PagerDuty::ProcessWebhookService, feature_category: :incident_management do
let_it_be(:project, reload: true) { create(:project) }
describe '#execute' do
diff --git a/spec/services/incident_management/timeline_event_tags/create_service_spec.rb b/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
index c1b993ce3d9..b21a116d5f9 100644
--- a/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::TimelineEventTags::CreateService do
+RSpec.describe IncidentManagement::TimelineEventTags::CreateService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be_with_reload(:project) { create(:project) }
diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb
index fa5f4c64a43..fff6241f083 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::TimelineEvents::CreateService do
+RSpec.describe IncidentManagement::TimelineEvents::CreateService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -57,7 +57,6 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
@@ -286,7 +285,6 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
diff --git a/spec/services/incident_management/timeline_events/destroy_service_spec.rb b/spec/services/incident_management/timeline_events/destroy_service_spec.rb
index f90ff72a2bf..78f6659beec 100644
--- a/spec/services/incident_management/timeline_events/destroy_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::TimelineEvents::DestroyService do
+RSpec.describe IncidentManagement::TimelineEvents::DestroyService, feature_category: :incident_management do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -67,7 +67,6 @@ RSpec.describe IncidentManagement::TimelineEvents::DestroyService do
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_deleted
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:user) { current_user }
diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb
index ebaa4dde7a2..c38126baa65 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -50,7 +50,6 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService, feature_catego
it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_edited
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace.reload }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_timeline_event_edited' }
diff --git a/spec/services/integrations/propagate_service_spec.rb b/spec/services/integrations/propagate_service_spec.rb
index c971c4a0ad0..0267b1b0ed0 100644
--- a/spec/services/integrations/propagate_service_spec.rb
+++ b/spec/services/integrations/propagate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::PropagateService do
+RSpec.describe Integrations::PropagateService, feature_category: :integrations do
describe '.propagate' do
include JiraIntegrationHelpers
diff --git a/spec/services/integrations/test/project_service_spec.rb b/spec/services/integrations/test/project_service_spec.rb
index 74833686283..4f8f932fb45 100644
--- a/spec/services/integrations/test/project_service_spec.rb
+++ b/spec/services/integrations/test/project_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::Test::ProjectService do
+RSpec.describe Integrations::Test::ProjectService, feature_category: :integrations do
include AfterNextHelpers
describe '#execute' do
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 7ba349ceeae..a76d575a1e0 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::BulkUpdateService do
+RSpec.describe Issuable::BulkUpdateService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, namespace: user.namespace) }
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index 0d2b8a4ac3c..9306aeaac44 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::CommonSystemNotesService do
+RSpec.describe Issuable::CommonSystemNotesService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/issuable/destroy_label_links_service_spec.rb b/spec/services/issuable/destroy_label_links_service_spec.rb
index bbc69e266c9..f0a92c201d2 100644
--- a/spec/services/issuable/destroy_label_links_service_spec.rb
+++ b/spec/services/issuable/destroy_label_links_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::DestroyLabelLinksService do
+RSpec.describe Issuable::DestroyLabelLinksService, feature_category: :team_planning do
describe '#execute' do
context 'when target is an Issue' do
let_it_be(:target) { create(:issue) }
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index 29f548e1c47..1acaf01dce0 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::DestroyService do
+RSpec.describe Issuable::DestroyService, feature_category: :team_planning do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index a6f57088ad1..03b6a1b4556 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::DiscussionsListService do
+RSpec.describe Issuable::DiscussionsListService, feature_category: :team_planning do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, :repository, :private, group: group) }
diff --git a/spec/services/issuable/process_assignees_spec.rb b/spec/services/issuable/process_assignees_spec.rb
index 9e909b68172..2c8d4c5e11d 100644
--- a/spec/services/issuable/process_assignees_spec.rb
+++ b/spec/services/issuable/process_assignees_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::ProcessAssignees do
+RSpec.describe Issuable::ProcessAssignees, feature_category: :team_planning do
describe '#execute' do
it 'returns assignee_ids when add_assignee_ids and remove_assignee_ids are not specified' do
process = Issuable::ProcessAssignees.new(assignee_ids: %w(5 7 9),
diff --git a/spec/services/issue_links/create_service_spec.rb b/spec/services/issue_links/create_service_spec.rb
index 0629b8b091b..71603da1c90 100644
--- a/spec/services/issue_links/create_service_spec.rb
+++ b/spec/services/issue_links/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssueLinks::CreateService do
+RSpec.describe IssueLinks::CreateService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create :user }
let_it_be(:namespace) { create :namespace }
@@ -43,7 +43,6 @@ RSpec.describe IssueLinks::CreateService do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_relate' }
diff --git a/spec/services/issue_links/destroy_service_spec.rb b/spec/services/issue_links/destroy_service_spec.rb
index ecb53b5cd31..5c4814f5ad1 100644
--- a/spec/services/issue_links/destroy_service_spec.rb
+++ b/spec/services/issue_links/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssueLinks::DestroyService do
+RSpec.describe IssueLinks::DestroyService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:project) { create(:project_empty_repo, :private) }
let_it_be(:user) { create(:user) }
@@ -27,7 +27,6 @@ RSpec.describe IssueLinks::DestroyService do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue_b.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_unrelate' }
diff --git a/spec/services/issue_links/list_service_spec.rb b/spec/services/issue_links/list_service_spec.rb
index 7a3ba845c7c..bfb6127ed56 100644
--- a/spec/services/issue_links/list_service_spec.rb
+++ b/spec/services/issue_links/list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssueLinks::ListService do
+RSpec.describe IssueLinks::ListService, feature_category: :team_planning do
let(:user) { create :user }
let(:project) { create(:project_empty_repo, :private) }
let(:issue) { create :issue, project: project }
diff --git a/spec/services/issues/after_create_service_spec.rb b/spec/services/issues/after_create_service_spec.rb
index 39a6799dbad..594caed23d7 100644
--- a/spec/services/issues/after_create_service_spec.rb
+++ b/spec/services/issues/after_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::AfterCreateService do
+RSpec.describe Issues::AfterCreateService, feature_category: :team_planning do
include AfterNextHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/services/issues/base_service_spec.rb b/spec/services/issues/base_service_spec.rb
new file mode 100644
index 00000000000..94165d557d8
--- /dev/null
+++ b/spec/services/issues/base_service_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issues::BaseService, feature_category: :team_planning do
+ describe '#constructor_container_arg' do
+ it { expect(described_class.constructor_container_arg("some-value")).to eq({ container: "some-value" }) }
+ end
+end
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 2160c45d079..0f89a746520 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::BuildService do
+RSpec.describe Issues::BuildService, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb
index eafaea93015..2fb14d8ce8e 100644
--- a/spec/services/issues/clone_service_spec.rb
+++ b/spec/services/issues/clone_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::CloneService do
+RSpec.describe Issues::CloneService, feature_category: :team_planning do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 803808e667c..0d9b3306540 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::CloseService do
+RSpec.describe Issues::CloseService, feature_category: :team_planning do
let(:project) { create(:project, :repository) }
let(:user) { create(:user, email: "user@example.com") }
let(:user2) { create(:user, email: "user2@example.com") }
@@ -100,7 +100,6 @@ RSpec.describe Issues::CloseService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_closed
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_closed' }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index ada5b300d7a..d5d88baca1f 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::CreateService do
+RSpec.describe Issues::CreateService, feature_category: :team_planning do
include AfterNextHelpers
let_it_be(:group) { create(:group, :crm_enabled) }
diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb
index f49bce70cd0..f9d8bf04ae9 100644
--- a/spec/services/issues/duplicate_service_spec.rb
+++ b/spec/services/issues/duplicate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::DuplicateService do
+RSpec.describe Issues::DuplicateService, feature_category: :team_planning do
let(:user) { create(:user) }
let(:canonical_project) { create(:project) }
let(:duplicate_project) { create(:project) }
diff --git a/spec/services/issues/import_csv_service_spec.rb b/spec/services/issues/import_csv_service_spec.rb
index 90e360f9cf1..6a147782209 100644
--- a/spec/services/issues/import_csv_service_spec.rb
+++ b/spec/services/issues/import_csv_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Issues::ImportCsvService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:assignee) { create(:user, username: 'csv_assignee') }
+ let(:file) { fixture_file_upload('spec/fixtures/csv_complex.csv') }
let(:service) do
uploader = FileUploader.new(project)
uploader.store!(file)
@@ -19,8 +20,6 @@ RSpec.describe Issues::ImportCsvService, feature_category: :team_planning do
end
describe '#execute' do
- let(:file) { fixture_file_upload('spec/fixtures/csv_complex.csv') }
-
subject { service.execute }
it 'sets all issueable attributes and executes quick actions' do
diff --git a/spec/services/issues/issuable_base_service_spec.rb b/spec/services/issues/issuable_base_service_spec.rb
new file mode 100644
index 00000000000..e1680d5c908
--- /dev/null
+++ b/spec/services/issues/issuable_base_service_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssuableBaseService, feature_category: :team_planning do
+ describe '#constructor_container_arg' do
+ it { expect(described_class.constructor_container_arg("some-value")).to eq({ container: "some-value" }) }
+ end
+end
diff --git a/spec/services/issues/prepare_import_csv_service_spec.rb b/spec/services/issues/prepare_import_csv_service_spec.rb
index ded23ee43b9..d318d4fd25e 100644
--- a/spec/services/issues/prepare_import_csv_service_spec.rb
+++ b/spec/services/issues/prepare_import_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::PrepareImportCsvService do
+RSpec.describe Issues::PrepareImportCsvService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb
index aee3583b834..4781daf7688 100644
--- a/spec/services/issues/referenced_merge_requests_service_spec.rb
+++ b/spec/services/issues/referenced_merge_requests_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ReferencedMergeRequestsService do
+RSpec.describe Issues::ReferencedMergeRequestsService, feature_category: :team_planning do
def create_referencing_mr(attributes = {})
create(:merge_request, attributes).tap do |merge_request|
create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb
index 05c61d0abfc..940d988668e 100644
--- a/spec/services/issues/related_branches_service_spec.rb
+++ b/spec/services/issues/related_branches_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::RelatedBranchesService do
+RSpec.describe Issues::RelatedBranchesService, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository, :public, public_builds: false) }
let_it_be(:developer) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb
index 27c0394ac8b..68f1af49b5f 100644
--- a/spec/services/issues/relative_position_rebalancing_service_spec.rb
+++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_shared_state do
+RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_shared_state, feature_category: :team_planning do
let_it_be(:project, reload: true) { create(:project, :repository_disabled, skip_disk_validation: true) }
let_it_be(:user) { project.creator }
let_it_be(:start) { RelativePositioning::START_POSITION }
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 68015a2327e..0f89844a2c1 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ReopenService do
+RSpec.describe Issues::ReopenService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:issue) { create(:issue, :closed, project: project) }
@@ -75,7 +75,6 @@ RSpec.describe Issues::ReopenService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_reopened
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_reopened' }
diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb
index 430a9e9f526..b98d23e0f7f 100644
--- a/spec/services/issues/reorder_service_spec.rb
+++ b/spec/services/issues/reorder_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ReorderService do
+RSpec.describe Issues::ReorderService, feature_category: :team_planning do
let_it_be(:user) { create_default(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, namespace: group) }
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 1ac71b966bc..c2111bffdda 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ResolveDiscussions do
+RSpec.describe Issues::ResolveDiscussions, feature_category: :team_planning do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -11,7 +11,7 @@ RSpec.describe Issues::ResolveDiscussions do
DummyService.class_eval do
include ::Issues::ResolveDiscussions
- def initialize(project:, current_user: nil, params: {})
+ def initialize(container:, current_user: nil, params: {})
super
filter_resolve_discussion_params
end
@@ -26,7 +26,7 @@ RSpec.describe Issues::ResolveDiscussions do
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") }
describe "#merge_request_for_resolving_discussion" do
- let(:service) { DummyService.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
+ let(:service) { DummyService.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
it "finds the merge request" do
expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
@@ -45,7 +45,7 @@ RSpec.describe Issues::ResolveDiscussions do
describe "#discussions_to_resolve" do
it "contains a single discussion when matching merge request and discussion are passed" do
service = DummyService.new(
- project: project,
+ container: project,
current_user: user,
params: {
discussion_to_resolve: discussion.id,
@@ -65,7 +65,7 @@ RSpec.describe Issues::ResolveDiscussions do
project: merge_request.target_project,
line_number: 15)])
service = DummyService.new(
- project: project,
+ container: project,
current_user: user,
params: { merge_request_to_resolve_discussions_of: merge_request.iid }
)
@@ -83,7 +83,7 @@ RSpec.describe Issues::ResolveDiscussions do
line_number: 15
)])
service = DummyService.new(
- project: project,
+ container: project,
current_user: user,
params: { merge_request_to_resolve_discussions_of: merge_request.iid }
)
@@ -96,7 +96,7 @@ RSpec.describe Issues::ResolveDiscussions do
it "is empty when a discussion and another merge request are passed" do
service = DummyService.new(
- project: project,
+ container: project,
current_user: user,
params: {
discussion_to_resolve: discussion.id,
diff --git a/spec/services/issues/set_crm_contacts_service_spec.rb b/spec/services/issues/set_crm_contacts_service_spec.rb
index 5613cc49cc5..aa5dec20a13 100644
--- a/spec/services/issues/set_crm_contacts_service_spec.rb
+++ b/spec/services/issues/set_crm_contacts_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::SetCrmContactsService do
+RSpec.describe Issues::SetCrmContactsService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:project) { create(:project, group: create(:group, :crm_enabled, parent: group)) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 973025bd2e3..9c81015e05e 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::UpdateService, :mailer do
+RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
@@ -191,7 +191,6 @@ RSpec.describe Issues::UpdateService, :mailer do
it_behaves_like 'an incident management tracked event', :incident_management_incident_change_confidential
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
@@ -696,7 +695,6 @@ RSpec.describe Issues::UpdateService, :mailer do
it_behaves_like 'an incident management tracked event', :incident_management_incident_assigned
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index 230e4c1b5e1..f2a81cbe33f 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ZoomLinkService do
+RSpec.describe Issues::ZoomLinkService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue) }
@@ -97,7 +97,6 @@ RSpec.describe Issues::ZoomLinkService do
it_behaves_like 'an incident management tracked event', :incident_management_incident_zoom_meeting
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_zoom_meeting' }
diff --git a/spec/services/jira/requests/projects/list_service_spec.rb b/spec/services/jira/requests/projects/list_service_spec.rb
index 78ee9cb9698..37e9f66d273 100644
--- a/spec/services/jira/requests/projects/list_service_spec.rb
+++ b/spec/services/jira/requests/projects/list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Jira::Requests::Projects::ListService do
+RSpec.describe Jira::Requests::Projects::ListService, feature_category: :projects do
include AfterNextHelpers
let(:jira_integration) { create(:jira_integration) }
diff --git a/spec/services/jira_connect/sync_service_spec.rb b/spec/services/jira_connect/sync_service_spec.rb
index 32580a7735f..fc1b4e997a5 100644
--- a/spec/services/jira_connect/sync_service_spec.rb
+++ b/spec/services/jira_connect/sync_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::SyncService do
+RSpec.describe JiraConnect::SyncService, feature_category: :integrations do
include AfterNextHelpers
describe '#execute' do
diff --git a/spec/services/jira_connect_installations/destroy_service_spec.rb b/spec/services/jira_connect_installations/destroy_service_spec.rb
index bb5bab53ccb..b8b59d6cc57 100644
--- a/spec/services/jira_connect_installations/destroy_service_spec.rb
+++ b/spec/services/jira_connect_installations/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnectInstallations::DestroyService do
+RSpec.describe JiraConnectInstallations::DestroyService, feature_category: :integrations do
describe '.execute' do
it 'creates an instance and calls execute' do
expect_next_instance_of(described_class, 'param1', 'param2', 'param3') do |destroy_service|
diff --git a/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb b/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb
index c621388a734..3c144de2208 100644
--- a/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb
+++ b/spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb
@@ -94,9 +94,9 @@ RSpec.describe JiraConnectInstallations::ProxyLifecycleEventService, feature_cat
expect(Gitlab::IntegrationsLogger).to receive(:info).with(
integration: 'JiraConnect',
message: 'Proxy lifecycle event received error response',
- event_type: evnet_type,
- status_code: 422,
- body: 'Error message'
+ jira_event_type: evnet_type,
+ jira_status_code: 422,
+ jira_body: 'Error message'
)
execute_service
diff --git a/spec/services/jira_connect_subscriptions/create_service_spec.rb b/spec/services/jira_connect_subscriptions/create_service_spec.rb
index 85208a30c30..f9d3954b84c 100644
--- a/spec/services/jira_connect_subscriptions/create_service_spec.rb
+++ b/spec/services/jira_connect_subscriptions/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnectSubscriptions::CreateService do
+RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integrations do
let_it_be(:installation) { create(:jira_connect_installation) }
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/jira_import/cloud_users_mapper_service_spec.rb b/spec/services/jira_import/cloud_users_mapper_service_spec.rb
index 6b06a982a80..e3f3d550467 100644
--- a/spec/services/jira_import/cloud_users_mapper_service_spec.rb
+++ b/spec/services/jira_import/cloud_users_mapper_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraImport::CloudUsersMapperService do
+RSpec.describe JiraImport::CloudUsersMapperService, feature_category: :integrations do
let(:start_at) { 7 }
let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
diff --git a/spec/services/jira_import/server_users_mapper_service_spec.rb b/spec/services/jira_import/server_users_mapper_service_spec.rb
index 71cb8aea0be..e2304953dd2 100644
--- a/spec/services/jira_import/server_users_mapper_service_spec.rb
+++ b/spec/services/jira_import/server_users_mapper_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraImport::ServerUsersMapperService do
+RSpec.describe JiraImport::ServerUsersMapperService, feature_category: :integrations do
let(:start_at) { 7 }
let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
diff --git a/spec/services/jira_import/start_import_service_spec.rb b/spec/services/jira_import/start_import_service_spec.rb
index c0db3012a30..9cb163e3d1a 100644
--- a/spec/services/jira_import/start_import_service_spec.rb
+++ b/spec/services/jira_import/start_import_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraImport::StartImportService do
+RSpec.describe JiraImport::StartImportService, feature_category: :integrations do
include JiraIntegrationHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/jira_import/users_importer_spec.rb b/spec/services/jira_import/users_importer_spec.rb
index ace9e0d5779..39f8475754a 100644
--- a/spec/services/jira_import/users_importer_spec.rb
+++ b/spec/services/jira_import/users_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraImport::UsersImporter do
+RSpec.describe JiraImport::UsersImporter, feature_category: :integrations do
include JiraIntegrationHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb
index 1dbe383ad8e..0a9fe2f5856 100644
--- a/spec/services/keys/create_service_spec.rb
+++ b/spec/services/keys/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Keys::CreateService do
+RSpec.describe Keys::CreateService, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:params) { attributes_for(:key) }
diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb
index dd40f9d73fd..9f064cb7236 100644
--- a/spec/services/keys/destroy_service_spec.rb
+++ b/spec/services/keys/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Keys::DestroyService do
+RSpec.describe Keys::DestroyService, feature_category: :source_code_management do
let(:user) { create(:user) }
subject { described_class.new(user) }
diff --git a/spec/services/keys/expiry_notification_service_spec.rb b/spec/services/keys/expiry_notification_service_spec.rb
index 7cb6cbce311..b8db4f28df7 100644
--- a/spec/services/keys/expiry_notification_service_spec.rb
+++ b/spec/services/keys/expiry_notification_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Keys::ExpiryNotificationService do
+RSpec.describe Keys::ExpiryNotificationService, feature_category: :source_code_management do
let_it_be_with_reload(:user) { create(:user) }
let(:params) { { keys: user.keys, expiring_soon: expiring_soon } }
diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb
index a2cd5ffdd38..3113fe27acf 100644
--- a/spec/services/keys/last_used_service_spec.rb
+++ b/spec/services/keys/last_used_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Keys::LastUsedService do
+RSpec.describe Keys::LastUsedService, feature_category: :source_code_management do
describe '#execute', :clean_gitlab_redis_shared_state do
it 'updates the key when it has not been used recently' do
key = create(:key, last_used_at: 1.year.ago)
diff --git a/spec/services/keys/revoke_service_spec.rb b/spec/services/keys/revoke_service_spec.rb
index ec07701b4b7..8294ec5bbd1 100644
--- a/spec/services/keys/revoke_service_spec.rb
+++ b/spec/services/keys/revoke_service_spec.rb
@@ -32,17 +32,4 @@ RSpec.describe Keys::RevokeService, feature_category: :source_code_management do
expect { service.execute(key) }.not_to change { signature.reload.verification_status }
expect(key).to be_persisted
end
-
- context 'when revoke_ssh_signatures disabled' do
- before do
- stub_feature_flags(revoke_ssh_signatures: false)
- end
-
- it 'does not unverifies signatures' do
- key = create(:key)
- signature = create(:ssh_signature, key: key)
-
- expect { service.execute(key) }.not_to change { signature.reload.verification_status }
- end
- end
end
diff --git a/spec/services/labels/available_labels_service_spec.rb b/spec/services/labels/available_labels_service_spec.rb
index 355dbd0c712..51314c2c226 100644
--- a/spec/services/labels/available_labels_service_spec.rb
+++ b/spec/services/labels/available_labels_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Labels::AvailableLabelsService do
+RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :public, group: group) }
let(:group) { create(:group) }
diff --git a/spec/services/labels/create_service_spec.rb b/spec/services/labels/create_service_spec.rb
index 02dec8ae690..9be611490cf 100644
--- a/spec/services/labels/create_service_spec.rb
+++ b/spec/services/labels/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Labels::CreateService do
+RSpec.describe Labels::CreateService, feature_category: :team_planning do
describe '#execute' do
let(:project) { create(:project) }
let(:group) { create(:group) }
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index 3ea2727dc60..0bc1326942d 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Labels::FindOrCreateService do
+RSpec.describe Labels::FindOrCreateService, feature_category: :team_planning do
describe '#execute' do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
index 3af6cf4c8f4..79cc88c65c8 100644
--- a/spec/services/labels/promote_service_spec.rb
+++ b/spec/services/labels/promote_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Labels::PromoteService do
+RSpec.describe Labels::PromoteService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index e67ab6025a5..bf895692e64 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Labels::TransferService do
+RSpec.describe Labels::TransferService, feature_category: :team_planning do
shared_examples 'transfer labels' do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb
index abc456f75f9..b9ac5282d10 100644
--- a/spec/services/labels/update_service_spec.rb
+++ b/spec/services/labels/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Labels::UpdateService do
+RSpec.describe Labels::UpdateService, feature_category: :team_planning do
describe '#execute' do
let(:project) { create(:project) }
diff --git a/spec/services/lfs/lock_file_service_spec.rb b/spec/services/lfs/lock_file_service_spec.rb
index b3a121866c8..47bf0c5f4ce 100644
--- a/spec/services/lfs/lock_file_service_spec.rb
+++ b/spec/services/lfs/lock_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Lfs::LockFileService do
+RSpec.describe Lfs::LockFileService, feature_category: :source_code_management do
let(:project) { create(:project) }
let(:current_user) { create(:user) }
diff --git a/spec/services/lfs/locks_finder_service_spec.rb b/spec/services/lfs/locks_finder_service_spec.rb
index 1167212eb69..38f8dadd38d 100644
--- a/spec/services/lfs/locks_finder_service_spec.rb
+++ b/spec/services/lfs/locks_finder_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Lfs::LocksFinderService do
+RSpec.describe Lfs::LocksFinderService, feature_category: :source_code_management do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:params) { {} }
diff --git a/spec/services/lfs/push_service_spec.rb b/spec/services/lfs/push_service_spec.rb
index f52bba94eea..1ec143a7fc9 100644
--- a/spec/services/lfs/push_service_spec.rb
+++ b/spec/services/lfs/push_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Lfs::PushService do
+RSpec.describe Lfs::PushService, feature_category: :source_code_management do
let(:logger) { service.send(:logger) }
let(:lfs_client) { service.send(:lfs_client) }
diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb
index 7ab269f897a..45fd1adcfb4 100644
--- a/spec/services/lfs/unlock_file_service_spec.rb
+++ b/spec/services/lfs/unlock_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Lfs::UnlockFileService do
+RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management do
let(:project) { create(:project) }
let(:current_user) { create(:user) }
let(:lock_author) { create(:user) }
diff --git a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
index 735f090d926..6eee83d5ee9 100644
--- a/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
+++ b/spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LooseForeignKeys::BatchCleanerService do
+RSpec.describe LooseForeignKeys::BatchCleanerService, feature_category: :database do
include MigrationsHelpers
def create_table_structure
diff --git a/spec/services/loose_foreign_keys/cleaner_service_spec.rb b/spec/services/loose_foreign_keys/cleaner_service_spec.rb
index 2cfd8385953..04f6270c5f2 100644
--- a/spec/services/loose_foreign_keys/cleaner_service_spec.rb
+++ b/spec/services/loose_foreign_keys/cleaner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LooseForeignKeys::CleanerService do
+RSpec.describe LooseForeignKeys::CleanerService, feature_category: :database do
let(:schema) { ApplicationRecord.connection.current_schema }
let(:deleted_records) do
[
diff --git a/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
index 1824f822ba8..af010547cc9 100644
--- a/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
+++ b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService do
+RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService, feature_category: :database do
include MigrationsHelpers
def create_table_structure
diff --git a/spec/services/markdown_content_rewriter_service_spec.rb b/spec/services/markdown_content_rewriter_service_spec.rb
index d94289856cf..bf15ef08647 100644
--- a/spec/services/markdown_content_rewriter_service_spec.rb
+++ b/spec/services/markdown_content_rewriter_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MarkdownContentRewriterService do
+RSpec.describe MarkdownContentRewriterService, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:source_parent) { create(:project, :public) }
let_it_be(:target_parent) { create(:project, :public) }
diff --git a/spec/services/markup/rendering_service_spec.rb b/spec/services/markup/rendering_service_spec.rb
index 99ab87f2072..952ee33da98 100644
--- a/spec/services/markup/rendering_service_spec.rb
+++ b/spec/services/markup/rendering_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Markup::RenderingService do
+RSpec.describe Markup::RenderingService, feature_category: :projects do
describe '#execute' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) do
@@ -111,5 +111,22 @@ RSpec.describe Markup::RenderingService do
is_expected.to eq(expected_html)
end
end
+
+ context 'with reStructuredText' do
+ let(:file_name) { 'foo.rst' }
+ let(:text) { "####\nPART\n####" }
+
+ it 'returns rendered html' do
+ is_expected.to eq("<h1>PART</h1>\n\n")
+ end
+
+ context 'when input has an invalid syntax' do
+ let(:text) { "####\nPART\n##" }
+
+ it 'uses a simple formatter for html' do
+ is_expected.to eq("<p>####\n<br>PART\n<br>##</p>")
+ end
+ end
+ end
end
end
diff --git a/spec/services/mattermost/create_team_service_spec.rb b/spec/services/mattermost/create_team_service_spec.rb
new file mode 100644
index 00000000000..b9e5162aab4
--- /dev/null
+++ b/spec/services/mattermost/create_team_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mattermost::CreateTeamService, feature_category: :integrations do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ subject { described_class.new(group, user) }
+
+ it 'creates a team' do
+ expect_next_instance_of(::Mattermost::Team) do |instance|
+ expect(instance).to receive(:create).with(name: anything, display_name: anything, type: anything)
+ end
+
+ subject.execute
+ end
+
+ it 'adds an error if a team could not be created' do
+ expect_next_instance_of(::Mattermost::Team) do |instance|
+ expect(instance).to receive(:create).and_raise(::Mattermost::ClientError, 'client error')
+ end
+
+ subject.execute
+
+ expect(group.errors).to be_present
+ end
+end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index ca5c052d032..de5074809cb 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::ApproveAccessRequestService do
+RSpec.describe Members::ApproveAccessRequestService, feature_category: :subgroups do
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
let(:current_user) { create(:user) }
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 756e1cf403c..13f233162cd 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state, :sidekiq_inline do
+RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state, :sidekiq_inline,
+ feature_category: :subgroups do
let_it_be(:source, reload: true) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:member) { create(:user) }
diff --git a/spec/services/members/creator_service_spec.rb b/spec/services/members/creator_service_spec.rb
index ad4c649086b..8191eefbe95 100644
--- a/spec/services/members/creator_service_spec.rb
+++ b/spec/services/members/creator_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::CreatorService do
+RSpec.describe Members::CreatorService, feature_category: :subgroups do
let_it_be(:source, reload: true) { create(:group, :public) }
let_it_be(:member_type) { GroupMember }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb
index fced7195046..48c971297c1 100644
--- a/spec/services/members/groups/creator_service_spec.rb
+++ b/spec/services/members/groups/creator_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::Groups::CreatorService do
+RSpec.describe Members::Groups::CreatorService, feature_category: :subgroups do
let_it_be(:source, reload: true) { create(:group, :public) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/members/import_project_team_service_spec.rb b/spec/services/members/import_project_team_service_spec.rb
index 96e8db1ba73..af9b30aa0b3 100644
--- a/spec/services/members/import_project_team_service_spec.rb
+++ b/spec/services/members/import_project_team_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::ImportProjectTeamService do
+RSpec.describe Members::ImportProjectTeamService, feature_category: :subgroups do
describe '#execute' do
let_it_be(:source_project) { create(:project) }
let_it_be(:target_project) { create(:project) }
diff --git a/spec/services/members/invitation_reminder_email_service_spec.rb b/spec/services/members/invitation_reminder_email_service_spec.rb
index 768a8719d54..da23965eabe 100644
--- a/spec/services/members/invitation_reminder_email_service_spec.rb
+++ b/spec/services/members/invitation_reminder_email_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::InvitationReminderEmailService do
+RSpec.describe Members::InvitationReminderEmailService, feature_category: :subgroups do
describe 'sending invitation reminders' do
subject { described_class.new(invitation).execute }
diff --git a/spec/services/members/invite_member_builder_spec.rb b/spec/services/members/invite_member_builder_spec.rb
index 52de65364c4..e7bbec4e0ef 100644
--- a/spec/services/members/invite_member_builder_spec.rb
+++ b/spec/services/members/invite_member_builder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::InviteMemberBuilder do
+RSpec.describe Members::InviteMemberBuilder, feature_category: :subgroups do
let_it_be(:source) { create(:group) }
let_it_be(:existing_member) { create(:group_member) }
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 23d4d671afc..22294b3fda5 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline do
+RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline,
+ feature_category: :subgroups do
let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { project.first_owner }
let_it_be(:project_user) { create(:user) }
diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb
index 5dfba7adf0f..f09682347ef 100644
--- a/spec/services/members/projects/creator_service_spec.rb
+++ b/spec/services/members/projects/creator_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::Projects::CreatorService do
+RSpec.describe Members::Projects::CreatorService, feature_category: :projects do
let_it_be(:source, reload: true) { create(:project, :public) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 69eea2aea4b..ef8ee6492ab 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::RequestAccessService do
+RSpec.describe Members::RequestAccessService, feature_category: :subgroups do
let(:user) { create(:user) }
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
diff --git a/spec/services/members/standard_member_builder_spec.rb b/spec/services/members/standard_member_builder_spec.rb
index 16daff53d31..69b764f3f16 100644
--- a/spec/services/members/standard_member_builder_spec.rb
+++ b/spec/services/members/standard_member_builder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::StandardMemberBuilder do
+RSpec.describe Members::StandardMemberBuilder, feature_category: :subgroups do
let_it_be(:source) { create(:group) }
let_it_be(:existing_member) { create(:group_member) }
diff --git a/spec/services/members/unassign_issuables_service_spec.rb b/spec/services/members/unassign_issuables_service_spec.rb
index 3f7ccb7bab3..37dfbd16c56 100644
--- a/spec/services/members/unassign_issuables_service_spec.rb
+++ b/spec/services/members/unassign_issuables_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::UnassignIssuablesService do
+RSpec.describe Members::UnassignIssuablesService, feature_category: :subgroups do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user, reload: true) { create(:user) }
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index 8a7f9a84c77..b94b44c8485 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::UpdateService do
+RSpec.describe Members::UpdateService, feature_category: :subgroups do
let_it_be(:project) { create(:project, :public) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:current_user) { create(:user) }
diff --git a/spec/services/merge_requests/add_context_service_spec.rb b/spec/services/merge_requests/add_context_service_spec.rb
index 448be27efe8..5fca2c17a3c 100644
--- a/spec/services/merge_requests/add_context_service_spec.rb
+++ b/spec/services/merge_requests/add_context_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AddContextService do
+RSpec.describe MergeRequests::AddContextService, feature_category: :code_review_workflow do
let(:project) { create(:project, :repository) }
let(:admin) { create(:admin) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: admin) }
diff --git a/spec/services/merge_requests/add_spent_time_service_spec.rb b/spec/services/merge_requests/add_spent_time_service_spec.rb
index 1e0b3e07f26..5d6d33c14d7 100644
--- a/spec/services/merge_requests/add_spent_time_service_spec.rb
+++ b/spec/services/merge_requests/add_spent_time_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AddSpentTimeService do
+RSpec.describe MergeRequests::AddSpentTimeService, feature_category: :code_review_workflow do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be_with_reload(:merge_request) { create(:merge_request, :simple, :unique_branches, source_project: project) }
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index 8d1abe5ea89..3c37792b576 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::MergeRequests::AddTodoWhenBuildFailsService do
+RSpec.describe ::MergeRequests::AddTodoWhenBuildFailsService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index 1d6427900b9..6140021c8d2 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ApprovalService do
+RSpec.describe MergeRequests::ApprovalService, feature_category: :code_review_workflow do
describe '#execute' do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, reviewers: [user]) }
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index cf405c0102e..f5fb88b6161 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AssignIssuesService do
+RSpec.describe MergeRequests::AssignIssuesService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/services/merge_requests/base_service_spec.rb b/spec/services/merge_requests/base_service_spec.rb
index bd907ba6015..deabb1c9804 100644
--- a/spec/services/merge_requests/base_service_spec.rb
+++ b/spec/services/merge_requests/base_service_spec.rb
@@ -123,4 +123,8 @@ RSpec.describe MergeRequests::BaseService, feature_category: :code_review_workfl
end
end
end
+
+ describe '#constructor_container_arg' do
+ it { expect(described_class.constructor_container_arg("some-value")).to eq({ project: "some-value" }) }
+ end
end
diff --git a/spec/services/merge_requests/cleanup_refs_service_spec.rb b/spec/services/merge_requests/cleanup_refs_service_spec.rb
index e8690ae5bf2..960b8101c36 100644
--- a/spec/services/merge_requests/cleanup_refs_service_spec.rb
+++ b/spec/services/merge_requests/cleanup_refs_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CleanupRefsService do
+RSpec.describe MergeRequests::CleanupRefsService, feature_category: :code_review_workflow do
describe '.schedule' do
let(:merge_request) { create(:merge_request) }
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 2c0817550c6..25c75ae7244 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -88,7 +88,11 @@ RSpec.describe MergeRequests::CloseService, feature_category: :code_review_workf
end
it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
- expect { execute }
+ expect do
+ execute
+
+ BatchLoader::Executor.clear_current
+ end
.to change { project.open_merge_requests_count }.from(1).to(0)
end
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index 5132eac0158..5eb53b1bcba 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Conflicts::ListService do
+RSpec.describe MergeRequests::Conflicts::ListService, feature_category: :code_review_workflow do
describe '#can_be_resolved_in_ui?' do
def create_merge_request(source_branch, target_branch = 'conflict-start')
create(:merge_request, source_branch: source_branch, target_branch: target_branch, merge_status: :unchecked) do |mr|
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 0abc70f71b0..e11cccaa54a 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Conflicts::ResolveService do
+RSpec.describe MergeRequests::Conflicts::ResolveService, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/services/merge_requests/create_approval_event_service_spec.rb b/spec/services/merge_requests/create_approval_event_service_spec.rb
index 3d41ace11a7..4876c992337 100644
--- a/spec/services/merge_requests/create_approval_event_service_spec.rb
+++ b/spec/services/merge_requests/create_approval_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateApprovalEventService do
+RSpec.describe MergeRequests::CreateApprovalEventService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index f11e3d0d1df..9c2321a2f16 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, :clean_gitlab_redis_cache do
+RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache, feature_category: :code_review_workflow do
include ProjectForksHelper
let_it_be(:project, refind: 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 394fc269ac3..7e20af32985 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -43,8 +43,11 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, f
end
it 'refreshes the number of open merge requests', :use_clean_rails_memory_store_caching do
- expect { service.execute }
- .to change { project.open_merge_requests_count }.from(0).to(1)
+ expect do
+ service.execute
+
+ BatchLoader::Executor.clear_current
+ end.to change { project.open_merge_requests_count }.from(0).to(1)
end
it 'creates exactly 1 create MR event', :sidekiq_might_not_need_inline do
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
index d2070a466b1..d9e60911ada 100644
--- a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_state do
+RSpec.describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_state,
+ feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let!(:subject) { described_class.new(merge_request) }
diff --git a/spec/services/merge_requests/execute_approval_hooks_service_spec.rb b/spec/services/merge_requests/execute_approval_hooks_service_spec.rb
index 863c47e8191..9f460648b05 100644
--- a/spec/services/merge_requests/execute_approval_hooks_service_spec.rb
+++ b/spec/services/merge_requests/execute_approval_hooks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ExecuteApprovalHooksService do
+RSpec.describe MergeRequests::ExecuteApprovalHooksService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index 5027acbba0a..993b7cfa85b 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::FfMergeService do
+RSpec.describe MergeRequests::FfMergeService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:merge_request) do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 5f81e1728fa..31b3e513a51 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe MergeRequests::GetUrlsService do
+RSpec.describe MergeRequests::GetUrlsService, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
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 3db3efedb84..951e59afe7f 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::HandleAssigneesChangeService do
+RSpec.describe MergeRequests::HandleAssigneesChangeService, feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:assignee) { create(:user) }
diff --git a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
index 8437876c3cf..172c2133168 100644
--- a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
+++ b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::MarkReviewerReviewedService do
+RSpec.describe MergeRequests::MarkReviewerReviewedService, feature_category: :code_review_workflow do
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request, reviewers: [current_user]) }
let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) }
diff --git a/spec/services/merge_requests/merge_orchestration_service_spec.rb b/spec/services/merge_requests/merge_orchestration_service_spec.rb
index ebcd2f0e277..41da7abfeab 100644
--- a/spec/services/merge_requests/merge_orchestration_service_spec.rb
+++ b/spec/services/merge_requests/merge_orchestration_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::MergeOrchestrationService do
+RSpec.describe MergeRequests::MergeOrchestrationService, feature_category: :code_review_workflow do
let_it_be(:maintainer) { create(:user) }
let(:merge_params) { { sha: merge_request.diff_head_sha } }
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index d3bf203d6bb..f380357a659 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::MergeService do
+RSpec.describe MergeRequests::MergeService, feature_category: :code_review_workflow do
include ExclusiveLeaseHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index 19fac3b5095..8428848c3c8 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::MergeToRefService do
+RSpec.describe MergeRequests::MergeToRefService, feature_category: :code_review_workflow do
shared_examples_for 'MergeService for target ref' do
it 'target_ref has the same state of target branch' do
repo = merge_request.target_project.repository
diff --git a/spec/services/merge_requests/mergeability/check_base_service_spec.rb b/spec/services/merge_requests/mergeability/check_base_service_spec.rb
index f07522b43cb..806bde61c23 100644
--- a/spec/services/merge_requests/mergeability/check_base_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::CheckBaseService do
+RSpec.describe MergeRequests::Mergeability::CheckBaseService, feature_category: :code_review_workflow do
subject(:check_base_service) { described_class.new(merge_request: merge_request, params: params) }
let(:merge_request) { double }
diff --git a/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb
index 6cc1079c94a..b6ee1049bb9 100644
--- a/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
+RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService, feature_category: :code_review_workflow do
subject(:check_broken_status) { described_class.new(merge_request: merge_request, params: {}) }
let(:merge_request) { build(:merge_request) }
diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
index def3cb0ca28..cf835cf70a3 100644
--- a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
+RSpec.describe MergeRequests::Mergeability::CheckCiStatusService, feature_category: :code_review_workflow do
subject(:check_ci_status) { described_class.new(merge_request: merge_request, params: params) }
let(:merge_request) { build(:merge_request) }
diff --git a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
index 9f107ce046a..a3b77558ec3 100644
--- a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
+RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService, feature_category: :code_review_workflow do
subject(:check_discussions_status) { described_class.new(merge_request: merge_request, params: params) }
let(:merge_request) { build(:merge_request) }
diff --git a/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb
index e9363e5d676..cb624705a02 100644
--- a/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
+RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService, feature_category: :code_review_workflow do
subject(:check_draft_status) { described_class.new(merge_request: merge_request, params: {}) }
let(:merge_request) { build(:merge_request) }
diff --git a/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb
index 936524b020a..53ad77ea4df 100644
--- a/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
+RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService, feature_category: :code_review_workflow do
subject(:check_open_status) { described_class.new(merge_request: merge_request, params: {}) }
let(:merge_request) { build(:merge_request) }
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
index 5722bb79cc5..876b1e7f23a 100644
--- a/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/detailed_merge_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::MergeRequests::Mergeability::DetailedMergeStatusService do
+RSpec.describe ::MergeRequests::Mergeability::DetailedMergeStatusService, feature_category: :code_review_workflow do
subject(:detailed_merge_status) { described_class.new(merge_request: merge_request).execute }
context 'when merge status is cannot_be_merged_rechecking' do
diff --git a/spec/services/merge_requests/mergeability/logger_spec.rb b/spec/services/merge_requests/mergeability/logger_spec.rb
index 3e2a1e9f9fd..1f56b6bebdb 100644
--- a/spec/services/merge_requests/mergeability/logger_spec.rb
+++ b/spec/services/merge_requests/mergeability/logger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::Logger, :request_store do
+RSpec.describe MergeRequests::Mergeability::Logger, :request_store, feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request) }
subject(:logger) { described_class.new(merge_request: merge_request) }
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 c56b38bccc1..bfff582994b 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::RunChecksService, :clean_gitlab_redis_cache do
+RSpec.describe MergeRequests::Mergeability::RunChecksService, :clean_gitlab_redis_cache, feature_category: :code_review_workflow do
subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) }
describe '#execute' do
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
index ee23238314e..881157e7f11 100644
--- a/spec/services/merge_requests/mergeability_check_service_spec.rb
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shared_state do
+RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shared_state, feature_category: :code_review_workflow do
shared_examples_for 'unmergeable merge request' do
it 'updates or keeps merge status as cannot_be_merged' do
subject
diff --git a/spec/services/merge_requests/migrate_external_diffs_service_spec.rb b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb
index 6ea8626ba73..c7f78dfa992 100644
--- a/spec/services/merge_requests/migrate_external_diffs_service_spec.rb
+++ b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::MigrateExternalDiffsService do
+RSpec.describe MergeRequests::MigrateExternalDiffsService, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let(:diff) { merge_request.merge_request_diff }
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index e486daae15e..f7526c169bd 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::PostMergeService do
+RSpec.describe MergeRequests::PostMergeService, feature_category: :code_review_workflow do
include ProjectForksHelper
let_it_be(:user) { create(:user) }
@@ -23,7 +23,11 @@ RSpec.describe MergeRequests::PostMergeService do
# Cache the counter before the MR changed state.
project.open_merge_requests_count
- expect { subject }.to change { project.open_merge_requests_count }.from(1).to(0)
+ expect do
+ subject
+
+ BatchLoader::Executor.clear_current
+ end.to change { project.open_merge_requests_count }.from(1).to(0)
end
it 'updates metrics' do
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index 251bf6f0d9d..7ca795ecd1a 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::PushOptionsHandlerService do
+RSpec.describe MergeRequests::PushOptionsHandlerService, feature_category: :source_code_management do
include ProjectForksHelper
let_it_be(:parent_group) { create(:group, :public) }
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 0814942b6b7..d8e5eb3bcde 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -109,6 +109,14 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
expect(@fork_build_failed_todo).to be_done
end
+ it 'triggers mergeRequestMergeStatusUpdated GraphQL subscription conditionally' do
+ expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(@merge_request)
+ expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(@another_merge_request)
+ expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated).with(@fork_merge_request)
+
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ end
+
context 'when a merge error exists' do
let(:error_message) { 'This is a merge error' }
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index 3d5b65207e6..4dd05f75a3e 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_caching do
+RSpec.describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_caching,
+feature_category: :code_review_workflow do
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:subject) { described_class.new(merge_request, current_user) }
diff --git a/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb b/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb
index 20b5cf5e3a1..1c315f12221 100644
--- a/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb
+++ b/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ReloadMergeHeadDiffService do
+RSpec.describe MergeRequests::ReloadMergeHeadDiffService, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
subject { described_class.new(merge_request).execute }
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index b9df31b6727..7399b29d06e 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ReopenService do
+RSpec.describe MergeRequests::ReopenService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
@@ -92,7 +92,11 @@ RSpec.describe MergeRequests::ReopenService do
it 'refreshes the number of open merge requests for a valid MR' do
service = described_class.new(project: project, current_user: user)
- expect { service.execute(merge_request) }
+ expect do
+ service.execute(merge_request)
+
+ BatchLoader::Executor.clear_current
+ end
.to change { project.open_merge_requests_count }.from(0).to(1)
end
diff --git a/spec/services/merge_requests/request_review_service_spec.rb b/spec/services/merge_requests/request_review_service_spec.rb
index 1d3f92b083f..ef96bf11e0b 100644
--- a/spec/services/merge_requests/request_review_service_spec.rb
+++ b/spec/services/merge_requests/request_review_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RequestReviewService do
+RSpec.describe MergeRequests::RequestReviewService, feature_category: :code_review_workflow do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, reviewers: [user]) }
diff --git a/spec/services/merge_requests/resolve_todos_service_spec.rb b/spec/services/merge_requests/resolve_todos_service_spec.rb
index 53bd259f0f4..de7ddbea8bb 100644
--- a/spec/services/merge_requests/resolve_todos_service_spec.rb
+++ b/spec/services/merge_requests/resolve_todos_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ResolveTodosService do
+RSpec.describe MergeRequests::ResolveTodosService, feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
index 2f191f2ee44..c3a99431dcc 100644
--- a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
+++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ResolvedDiscussionNotificationService do
+RSpec.describe MergeRequests::ResolvedDiscussionNotificationService, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index 471bb03f18c..26e225db9fc 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::SquashService do
+RSpec.describe MergeRequests::SquashService, feature_category: :source_code_management do
let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request }) }
let(:user) { project.first_owner }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb
index 2d80d75a262..7d08eb86441 100644
--- a/spec/services/merge_requests/update_assignees_service_spec.rb
+++ b/spec/services/merge_requests/update_assignees_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::UpdateAssigneesService do
+RSpec.describe MergeRequests::UpdateAssigneesService, feature_category: :code_review_workflow do
include AfterNextHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/services/merge_requests/update_reviewers_service_spec.rb b/spec/services/merge_requests/update_reviewers_service_spec.rb
index 9f935e1cecf..ed2d448b523 100644
--- a/spec/services/merge_requests/update_reviewers_service_spec.rb
+++ b/spec/services/merge_requests/update_reviewers_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::UpdateReviewersService do
+RSpec.describe MergeRequests::UpdateReviewersService, feature_category: :code_review_workflow do
include AfterNextHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/services/metrics/dashboard/annotations/create_service_spec.rb b/spec/services/metrics/dashboard/annotations/create_service_spec.rb
index 8f5484fcabe..2bcfa54ead7 100644
--- a/spec/services/metrics/dashboard/annotations/create_service_spec.rb
+++ b/spec/services/metrics/dashboard/annotations/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::Annotations::CreateService do
+RSpec.describe Metrics::Dashboard::Annotations::CreateService, feature_category: :metrics do
let_it_be(:user) { create(:user) }
let(:description) { 'test annotation' }
diff --git a/spec/services/metrics/dashboard/annotations/delete_service_spec.rb b/spec/services/metrics/dashboard/annotations/delete_service_spec.rb
index ec2bd3772bf..557d6d95767 100644
--- a/spec/services/metrics/dashboard/annotations/delete_service_spec.rb
+++ b/spec/services/metrics/dashboard/annotations/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::Annotations::DeleteService do
+RSpec.describe Metrics::Dashboard::Annotations::DeleteService, feature_category: :metrics do
let(:user) { create(:user) }
let(:service_instance) { described_class.new(user, annotation) }
diff --git a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
index 47e5557105b..40ec37c393d 100644
--- a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_store_caching, feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb b/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb
index f2e32d5eb35..beed23a366f 100644
--- a/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/cluster_dashboard_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::ClusterDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::ClusterDashboardService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb b/spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb
index dbb89af45d0..5d63505e5cc 100644
--- a/spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/cluster_metrics_embed_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::ClusterMetricsEmbedService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::ClusterMetricsEmbedService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/services/metrics/dashboard/custom_dashboard_service_spec.rb b/spec/services/metrics/dashboard/custom_dashboard_service_spec.rb
index afeb1646005..940daa38ae7 100644
--- a/spec/services/metrics/dashboard/custom_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/custom_dashboard_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::CustomDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::CustomDashboardService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb
index 127cec6275c..8117296b048 100644
--- a/spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::CustomMetricEmbedService do
+RSpec.describe Metrics::Dashboard::CustomMetricEmbedService, feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:project, reload: true) { build(:project) }
diff --git a/spec/services/metrics/dashboard/default_embed_service_spec.rb b/spec/services/metrics/dashboard/default_embed_service_spec.rb
index 647778eadc1..6ef248f6b09 100644
--- a/spec/services/metrics/dashboard/default_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/default_embed_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::DefaultEmbedService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::DefaultEmbedService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:project) { build(:project) }
diff --git a/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb b/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
index 5eb8f24266c..1643f552a70 100644
--- a/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::DynamicEmbedService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::DynamicEmbedService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:project) { build(:project) }
diff --git a/spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb b/spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb
index 2905e4599f3..25812a492b2 100644
--- a/spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::GitlabAlertEmbedService do
+RSpec.describe Metrics::Dashboard::GitlabAlertEmbedService, feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:alert) { create(:prometheus_alert) }
diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
index 5263fd40a40..877a455ea44 100644
--- a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
+RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService, feature_category: :metrics do
include MetricsDashboardHelpers
include ReactiveCachingHelpers
include GrafanaApiHelpers
diff --git a/spec/services/metrics/dashboard/panel_preview_service_spec.rb b/spec/services/metrics/dashboard/panel_preview_service_spec.rb
index 787c61cc918..2a70c667ee5 100644
--- a/spec/services/metrics/dashboard/panel_preview_service_spec.rb
+++ b/spec/services/metrics/dashboard/panel_preview_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::PanelPreviewService do
+RSpec.describe Metrics::Dashboard::PanelPreviewService, feature_category: :metrics do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:panel_yml) do
diff --git a/spec/services/metrics/dashboard/pod_dashboard_service_spec.rb b/spec/services/metrics/dashboard/pod_dashboard_service_spec.rb
index 0ea812e93ee..d26b27d7a18 100644
--- a/spec/services/metrics/dashboard/pod_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/pod_dashboard_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::PodDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::PodDashboardService, :use_clean_rails_memory_store_caching,
+ feature_category: :pods do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/dashboard/self_monitoring_dashboard_service_spec.rb b/spec/services/metrics/dashboard/self_monitoring_dashboard_service_spec.rb
index d0cefdbeb30..930789ed701 100644
--- a/spec/services/metrics/dashboard/self_monitoring_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/self_monitoring_dashboard_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::SelfMonitoringDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::SelfMonitoringDashboardService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/dashboard/system_dashboard_service_spec.rb b/spec/services/metrics/dashboard/system_dashboard_service_spec.rb
index e1c6aaeec66..b08b980e50e 100644
--- a/spec/services/metrics/dashboard/system_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/system_dashboard_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/dashboard/transient_embed_service_spec.rb b/spec/services/metrics/dashboard/transient_embed_service_spec.rb
index 53ea83c33d6..1e3ccde6ae3 100644
--- a/spec/services/metrics/dashboard/transient_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/transient_embed_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::TransientEmbedService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::TransientEmbedService, :use_clean_rails_memory_store_caching,
+ feature_category: :metrics do
let_it_be(:project) { build(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:environment) { create(:environment, project: project) }
diff --git a/spec/services/metrics/dashboard/update_dashboard_service_spec.rb b/spec/services/metrics/dashboard/update_dashboard_service_spec.rb
index 148005480ea..15bbe9f9364 100644
--- a/spec/services/metrics/dashboard/update_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/update_dashboard_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_store_caching do
+RSpec.describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_store_caching, feature_category: :metrics do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/services/metrics/sample_metrics_service_spec.rb b/spec/services/metrics/sample_metrics_service_spec.rb
index b94345500f0..3442b4303db 100644
--- a/spec/services/metrics/sample_metrics_service_spec.rb
+++ b/spec/services/metrics/sample_metrics_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::SampleMetricsService do
+RSpec.describe Metrics::SampleMetricsService, feature_category: :metrics do
describe 'query' do
let(:range_start) { '2019-12-02T23:31:45.000Z' }
let(:range_end) { '2019-12-03T00:01:45.000Z' }
diff --git a/spec/services/metrics/users_starred_dashboards/create_service_spec.rb b/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
index 1435e39e458..e08bdca8410 100644
--- a/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
+++ b/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::UsersStarredDashboards::CreateService do
+RSpec.describe Metrics::UsersStarredDashboards::CreateService, feature_category: :metrics do
let_it_be(:user) { create(:user) }
let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
diff --git a/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb b/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb
index 5cdffe681eb..8c4bcecc239 100644
--- a/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb
+++ b/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::UsersStarredDashboards::DeleteService do
+RSpec.describe Metrics::UsersStarredDashboards::DeleteService, feature_category: :metrics do
subject(:service_instance) { described_class.new(user, project, dashboard_path) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 53751b40667..f362c8da642 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestones::CloseService do
+RSpec.describe Milestones::CloseService, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) }
diff --git a/spec/services/milestones/closed_issues_count_service_spec.rb b/spec/services/milestones/closed_issues_count_service_spec.rb
index a3865d08972..f0ed0872c2d 100644
--- a/spec/services/milestones/closed_issues_count_service_spec.rb
+++ b/spec/services/milestones/closed_issues_count_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Milestones::ClosedIssuesCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Milestones::ClosedIssuesCountService, :use_clean_rails_memory_store_caching,
+ feature_category: :team_planning do
let(:project) { create(:project) }
let(:milestone) { create(:milestone, project: project) }
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index 93ca4ff653f..78cb05532eb 100644
--- a/spec/services/milestones/create_service_spec.rb
+++ b/spec/services/milestones/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestones::CreateService do
+RSpec.describe Milestones::CreateService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 6c08b7db43a..209177c348b 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestones::DestroyService do
+RSpec.describe Milestones::DestroyService, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
diff --git a/spec/services/milestones/find_or_create_service_spec.rb b/spec/services/milestones/find_or_create_service_spec.rb
index 1bcaf578441..8a72778a22a 100644
--- a/spec/services/milestones/find_or_create_service_spec.rb
+++ b/spec/services/milestones/find_or_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestones::FindOrCreateService do
+RSpec.describe Milestones::FindOrCreateService, feature_category: :team_planning do
describe '#execute' do
subject(:service) { described_class.new(project, user, params) }
diff --git a/spec/services/milestones/issues_count_service_spec.rb b/spec/services/milestones/issues_count_service_spec.rb
index c944055e4e7..a80b27822b6 100644
--- a/spec/services/milestones/issues_count_service_spec.rb
+++ b/spec/services/milestones/issues_count_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Milestones::IssuesCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Milestones::IssuesCountService, :use_clean_rails_memory_store_caching,
+ feature_category: :team_planning do
let(:project) { create(:project) }
let(:milestone) { create(:milestone, project: project) }
diff --git a/spec/services/milestones/merge_requests_count_service_spec.rb b/spec/services/milestones/merge_requests_count_service_spec.rb
index aecc7d5ef52..00b7b95aeb6 100644
--- a/spec/services/milestones/merge_requests_count_service_spec.rb
+++ b/spec/services/milestones/merge_requests_count_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Milestones::MergeRequestsCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Milestones::MergeRequestsCountService, :use_clean_rails_memory_store_caching,
+ feature_category: :team_planning do
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:milestone) { create(:milestone, project: project) }
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
index 8f4201d8d94..203ac2d3f40 100644
--- a/spec/services/milestones/promote_service_spec.rb
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestones::PromoteService do
+RSpec.describe Milestones::PromoteService, feature_category: :team_planning do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:user) { create(:user) }
diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb
index de02226661c..ea65f713902 100644
--- a/spec/services/milestones/transfer_service_spec.rb
+++ b/spec/services/milestones/transfer_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Milestones::TransferService do
+RSpec.describe Milestones::TransferService, feature_category: :team_planning do
describe '#execute' do
subject(:service) { described_class.new(user, old_group, project) }
diff --git a/spec/services/milestones/update_service_spec.rb b/spec/services/milestones/update_service_spec.rb
index 85fd89c11ac..76110af2514 100644
--- a/spec/services/milestones/update_service_spec.rb
+++ b/spec/services/milestones/update_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Milestones::UpdateService do
+RSpec.describe Milestones::UpdateService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { build(:user) }
let(:milestone) { create(:milestone, project: project) }
diff --git a/spec/services/ml/experiment_tracking/candidate_repository_spec.rb b/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
index e3c05178025..45a4792426c 100644
--- a/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
+++ b/spec/services/ml/experiment_tracking/candidate_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ml::ExperimentTracking::CandidateRepository do
+RSpec.describe ::Ml::ExperimentTracking::CandidateRepository, feature_category: :experimentation_activation do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:experiment) { create(:ml_experiments, user: user, project: project) }
diff --git a/spec/services/ml/experiment_tracking/experiment_repository_spec.rb b/spec/services/ml/experiment_tracking/experiment_repository_spec.rb
index c3c716b831a..3c645fa84b4 100644
--- a/spec/services/ml/experiment_tracking/experiment_repository_spec.rb
+++ b/spec/services/ml/experiment_tracking/experiment_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ml::ExperimentTracking::ExperimentRepository do
+RSpec.describe ::Ml::ExperimentTracking::ExperimentRepository, feature_category: :experimentation_activation do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:experiment) { create(:ml_experiments, user: user, project: project) }
diff --git a/spec/services/namespace_settings/update_service_spec.rb b/spec/services/namespace_settings/update_service_spec.rb
index e0f32cb3821..5f1ff6746bc 100644
--- a/spec/services/namespace_settings/update_service_spec.rb
+++ b/spec/services/namespace_settings/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NamespaceSettings::UpdateService do
+RSpec.describe NamespaceSettings::UpdateService, feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:settings) { {} }
diff --git a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
index b44c256802f..8a2ecd5c3e0 100644
--- a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
+++ b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
+RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute', feature_category: :purchase do
subject(:execute_service) { described_class.new(track, interval).execute }
let(:track) { :create }
diff --git a/spec/services/namespaces/package_settings/update_service_spec.rb b/spec/services/namespaces/package_settings/update_service_spec.rb
index 10926c5ef57..e21c9a8f1b9 100644
--- a/spec/services/namespaces/package_settings/update_service_spec.rb
+++ b/spec/services/namespaces/package_settings/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Namespaces::PackageSettings::UpdateService do
+RSpec.describe ::Namespaces::PackageSettings::UpdateService, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:namespace) { create(:group) }
diff --git a/spec/services/namespaces/statistics_refresher_service_spec.rb b/spec/services/namespaces/statistics_refresher_service_spec.rb
index 2d5f9235bd4..750f98615cc 100644
--- a/spec/services/namespaces/statistics_refresher_service_spec.rb
+++ b/spec/services/namespaces/statistics_refresher_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
+RSpec.describe Namespaces::StatisticsRefresherService, '#execute', feature_category: :subgroups do
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let(:projects) { create_list(:project, 5, namespace: group) }
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index ad244f62292..1cbbb68205d 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NoteSummary do
+RSpec.describe NoteSummary, feature_category: :code_review_workflow do
let(:project) { build(:project) }
let(:noteable) { build(:issue) }
let(:user) { build(:user) }
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index 67d8b37f809..97c736e373b 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::BuildService do
+RSpec.describe Notes::BuildService, feature_category: :team_planning do
include AdminModeHelper
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/notes/copy_service_spec.rb b/spec/services/notes/copy_service_spec.rb
index 2fa9a462bb9..5a48f6e7560 100644
--- a/spec/services/notes/copy_service_spec.rb
+++ b/spec/services/notes/copy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::CopyService do
+RSpec.describe Notes::CopyService, feature_category: :team_planning do
describe '#initialize' do
let_it_be(:noteable) { create(:issue) }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 1ee9e51433e..05a41ddc6c5 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -108,7 +108,6 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { issue.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_comment' }
@@ -124,10 +123,6 @@ RSpec.describe Notes::CreateService, feature_category: :team_planning do
let(:execute_create_service) { described_class.new(project, user, opts).execute }
- before do
- stub_feature_flags(notes_create_service_tracking: false)
- end
-
it 'tracks commit comment usage data', :clean_gitlab_redis_shared_state do
expect(counter).to receive(:count).with(:create, 'Commit').and_call_original
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index 744808525f5..43132b4221f 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::DestroyService do
+RSpec.describe Notes::DestroyService, feature_category: :team_planning do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index 17001733c5b..0bcfd6b63d2 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::PostProcessService do
+RSpec.describe Notes::PostProcessService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index bca954c3959..b474285e67e 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::QuickActionsService do
+RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
shared_context 'note on noteable' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
@@ -253,6 +253,12 @@ RSpec.describe Notes::QuickActionsService do
describe '.noteable_update_service_class' do
include_context 'note on noteable'
+ it 'returns WorkItems::UpdateService for a note on a work item' do
+ note = create(:note_on_work_item, project: project)
+
+ expect(described_class.noteable_update_service_class(note)).to eq(WorkItems::UpdateService)
+ end
+
it 'returns Issues::UpdateService for a note on an issue' do
note = create(:note_on_issue, project: project)
@@ -322,6 +328,84 @@ RSpec.describe Notes::QuickActionsService do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
end
+
+ context 'note on work item that supports quick actions' do
+ include_context 'note on noteable'
+
+ let_it_be(:work_item, reload: true) { create(:work_item, project: project) }
+
+ let(:note) { build(:note_on_work_item, project: project, noteable: work_item) }
+
+ let!(:labels) { create_pair(:label, project: project) }
+
+ before do
+ note.note = note_text
+ end
+
+ describe 'note with only command' do
+ describe '/close, /label & /assign' do
+ let(:note_text) do
+ %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n)
+ end
+
+ it 'closes noteable, sets labels, assigns and leave no note' do
+ content = execute(note)
+
+ expect(content).to be_empty
+ expect(note.noteable).to be_closed
+ expect(note.noteable.labels).to match_array(labels)
+ expect(note.noteable.assignees).to eq([assignee])
+ end
+ end
+
+ describe '/reopen' do
+ before do
+ note.noteable.close!
+ expect(note.noteable).to be_closed
+ end
+ let(:note_text) { '/reopen' }
+
+ it 'opens the noteable, and leave no note' do
+ content = execute(note)
+
+ expect(content).to be_empty
+ expect(note.noteable).to be_open
+ end
+ end
+ end
+
+ describe 'note with command & text' do
+ describe '/close, /label, /assign' do
+ let(:note_text) do
+ %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\nWORLD)
+ end
+
+ it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
+ content = execute(note)
+
+ expect(content).to eq "HELLO\nWORLD"
+ expect(note.noteable).to be_closed
+ expect(note.noteable.labels).to match_array(labels)
+ expect(note.noteable.assignees).to eq([assignee])
+ end
+ end
+
+ describe '/reopen' do
+ before do
+ note.noteable.close
+ expect(note.noteable).to be_closed
+ end
+ let(:note_text) { "HELLO\n/reopen\nWORLD" }
+
+ it 'opens the noteable' do
+ content = execute(note)
+
+ expect(content).to eq "HELLO\nWORLD"
+ expect(note.noteable).to be_open
+ end
+ end
+ end
+ end
end
context 'CE restriction for issue assignees' do
diff --git a/spec/services/notes/render_service_spec.rb b/spec/services/notes/render_service_spec.rb
index 09cd7dc572b..8ebecddffa7 100644
--- a/spec/services/notes/render_service_spec.rb
+++ b/spec/services/notes/render_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::RenderService do
+RSpec.describe Notes::RenderService, feature_category: :team_planning do
describe '#execute' do
it 'renders a Note' do
note = double(:note)
diff --git a/spec/services/notes/resolve_service_spec.rb b/spec/services/notes/resolve_service_spec.rb
index 1c5b308aed1..1b5586ee1b3 100644
--- a/spec/services/notes/resolve_service_spec.rb
+++ b/spec/services/notes/resolve_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::ResolveService do
+RSpec.describe Notes::ResolveService, feature_category: :team_planning do
let(:merge_request) { create(:merge_request) }
let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) }
let(:user) { merge_request.author }
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index 05703ac548d..0d5a0ae7706 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::UpdateService do
+RSpec.describe Notes::UpdateService, feature_category: :team_planning do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
let(:private_group) { create(:group, :private) }
diff --git a/spec/services/notification_recipients/build_service_spec.rb b/spec/services/notification_recipients/build_service_spec.rb
index 899d23ec641..bfd1dcd7d80 100644
--- a/spec/services/notification_recipients/build_service_spec.rb
+++ b/spec/services/notification_recipients/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NotificationRecipients::BuildService do
+RSpec.describe NotificationRecipients::BuildService, feature_category: :team_planning do
let(:service) { described_class }
let(:assignee) { create(:user) }
let(:project) { create(:project, :public) }
diff --git a/spec/services/notification_recipients/builder/default_spec.rb b/spec/services/notification_recipients/builder/default_spec.rb
index 4d0ddc7c4f7..da991b5951a 100644
--- a/spec/services/notification_recipients/builder/default_spec.rb
+++ b/spec/services/notification_recipients/builder/default_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NotificationRecipients::Builder::Default do
+RSpec.describe NotificationRecipients::Builder::Default, feature_category: :team_planning do
describe '#build!' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group).tap { |p| p.add_developer(project_watcher) if project_watcher } }
diff --git a/spec/services/notification_recipients/builder/new_note_spec.rb b/spec/services/notification_recipients/builder/new_note_spec.rb
index 7d2a4f682c5..e87824f3156 100644
--- a/spec/services/notification_recipients/builder/new_note_spec.rb
+++ b/spec/services/notification_recipients/builder/new_note_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NotificationRecipients::Builder::NewNote do
+RSpec.describe NotificationRecipients::Builder::NewNote, feature_category: :team_planning do
describe '#notification_recipients' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
diff --git a/spec/services/onboarding/progress_service_spec.rb b/spec/services/onboarding/progress_service_spec.rb
index 8f3f723613e..e1d6b4cd44b 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 Onboarding::ProgressService do
+RSpec.describe Onboarding::ProgressService, feature_category: :onboarding do
describe '.async' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:action) { :git_pull }
diff --git a/spec/services/packages/cleanup/execute_policy_service_spec.rb b/spec/services/packages/cleanup/execute_policy_service_spec.rb
index 93335c4a821..a083dc0d4ea 100644
--- a/spec/services/packages/cleanup/execute_policy_service_spec.rb
+++ b/spec/services/packages/cleanup/execute_policy_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Cleanup::ExecutePolicyService do
+RSpec.describe Packages::Cleanup::ExecutePolicyService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:policy) { create(:packages_cleanup_policy, project: project) }
diff --git a/spec/services/packages/cleanup/update_policy_service_spec.rb b/spec/services/packages/cleanup/update_policy_service_spec.rb
index a11fbb766f5..8068c351e5f 100644
--- a/spec/services/packages/cleanup/update_policy_service_spec.rb
+++ b/spec/services/packages/cleanup/update_policy_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Cleanup::UpdatePolicyService do
+RSpec.describe Packages::Cleanup::UpdatePolicyService, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
diff --git a/spec/services/packages/composer/composer_json_service_spec.rb b/spec/services/packages/composer/composer_json_service_spec.rb
index d2187688c4c..15acd79c49e 100644
--- a/spec/services/packages/composer/composer_json_service_spec.rb
+++ b/spec/services/packages/composer/composer_json_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Composer::ComposerJsonService do
+RSpec.describe Packages::Composer::ComposerJsonService, feature_category: :package_registry do
describe '#execute' do
let(:branch) { project.repository.find_branch('master') }
let(:target) { branch.target }
diff --git a/spec/services/packages/composer/create_package_service_spec.rb b/spec/services/packages/composer/create_package_service_spec.rb
index 26429a7b5d9..78d5d76fe4f 100644
--- a/spec/services/packages/composer/create_package_service_spec.rb
+++ b/spec/services/packages/composer/create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Composer::CreatePackageService do
+RSpec.describe Packages::Composer::CreatePackageService, feature_category: :package_registry do
include PackagesManagerApiSpecHelpers
let_it_be(:package_name) { 'composer-package-name' }
diff --git a/spec/services/packages/composer/version_parser_service_spec.rb b/spec/services/packages/composer/version_parser_service_spec.rb
index 69253ff934e..ac50f2e2e55 100644
--- a/spec/services/packages/composer/version_parser_service_spec.rb
+++ b/spec/services/packages/composer/version_parser_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Composer::VersionParserService do
+RSpec.describe Packages::Composer::VersionParserService, feature_category: :package_registry do
let_it_be(:params) { {} }
describe '#execute' do
diff --git a/spec/services/packages/conan/create_package_file_service_spec.rb b/spec/services/packages/conan/create_package_file_service_spec.rb
index e655b8d1f9e..6859e52560a 100644
--- a/spec/services/packages/conan/create_package_file_service_spec.rb
+++ b/spec/services/packages/conan/create_package_file_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Conan::CreatePackageFileService do
+RSpec.describe Packages::Conan::CreatePackageFileService, feature_category: :package_registry do
include WorkhorseHelpers
let_it_be(:package) { create(:conan_package) }
diff --git a/spec/services/packages/conan/create_package_service_spec.rb b/spec/services/packages/conan/create_package_service_spec.rb
index 6f644f5ef95..db06463b7fa 100644
--- a/spec/services/packages/conan/create_package_service_spec.rb
+++ b/spec/services/packages/conan/create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Conan::CreatePackageService do
+RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb
index f95e21cd045..06a7a13bdd9 100644
--- a/spec/services/packages/create_dependency_service_spec.rb
+++ b/spec/services/packages/create_dependency_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::CreateDependencyService do
+RSpec.describe Packages::CreateDependencyService, feature_category: :package_registry do
describe '#execute' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:version) { '1.0.1' }
diff --git a/spec/services/packages/create_event_service_spec.rb b/spec/services/packages/create_event_service_spec.rb
index 58fa68b11fe..44ad3f29c58 100644
--- a/spec/services/packages/create_event_service_spec.rb
+++ b/spec/services/packages/create_event_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::CreateEventService do
+RSpec.describe Packages::CreateEventService, feature_category: :package_registry do
let(:scope) { 'generic' }
let(:event_name) { 'push_package' }
diff --git a/spec/services/packages/create_package_file_service_spec.rb b/spec/services/packages/create_package_file_service_spec.rb
index 2ff00ea8568..5b4ea3e1530 100644
--- a/spec/services/packages/create_package_file_service_spec.rb
+++ b/spec/services/packages/create_package_file_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::CreatePackageFileService do
+RSpec.describe Packages::CreatePackageFileService, feature_category: :package_registry do
let_it_be(:package) { create(:maven_package) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/packages/create_temporary_package_service_spec.rb b/spec/services/packages/create_temporary_package_service_spec.rb
index 4b8d37401d8..be8b5afc1e0 100644
--- a/spec/services/packages/create_temporary_package_service_spec.rb
+++ b/spec/services/packages/create_temporary_package_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::CreateTemporaryPackageService do
+RSpec.describe Packages::CreateTemporaryPackageService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:params) { {} }
diff --git a/spec/services/packages/debian/create_package_file_service_spec.rb b/spec/services/packages/debian/create_package_file_service_spec.rb
index 43928669eb1..b527bf8c1de 100644
--- a/spec/services/packages/debian/create_package_file_service_spec.rb
+++ b/spec/services/packages/debian/create_package_file_service_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe Packages::Debian::CreatePackageFileService, feature_category: :pa
expect(package_file).to be_valid
expect(package_file.file.read).to start_with('Format: 1.8')
- expect(package_file.size).to eq(2143)
+ expect(package_file.size).to eq(2422)
expect(package_file.file_name).to eq(file_name)
expect(package_file.file_sha1).to eq('54321')
expect(package_file.file_sha256).to eq('543212345')
diff --git a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
index 4d6acac219b..a22c1fc7acc 100644
--- a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService, feature_category
expect(subject[:file_type]).to eq(:changes)
expect(subject[:architecture]).to be_nil
expect(subject[:fields]).to include(expected_fields)
- expect(subject[:files].count).to eq(6)
+ expect(subject[:files].count).to eq(7)
end
end
diff --git a/spec/services/packages/debian/extract_metadata_service_spec.rb b/spec/services/packages/debian/extract_metadata_service_spec.rb
index 412f285152b..1983c49c6b7 100644
--- a/spec/services/packages/debian/extract_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_metadata_service_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe Packages::Debian::ExtractMetadataService, feature_category: :package_registry do
@@ -6,12 +7,13 @@ RSpec.describe Packages::Debian::ExtractMetadataService, feature_category: :pack
subject { service.execute }
- RSpec.shared_context 'Debian ExtractMetadata Service' do |trait|
+ RSpec.shared_context 'with Debian package file' do |trait|
let(:package_file) { create(:debian_package_file, trait) }
end
RSpec.shared_examples 'Test Debian ExtractMetadata Service' do |expected_file_type, expected_architecture, expected_fields|
- it "returns file_type #{expected_file_type.inspect}, architecture #{expected_architecture.inspect} and fields #{expected_fields.nil? ? '' : 'including '}#{expected_fields.inspect}", :aggregate_failures do
+ it "returns file_type #{expected_file_type.inspect}, architecture #{expected_architecture.inspect} and fields #{expected_fields.nil? ? '' : 'including '}#{expected_fields.inspect}",
+ :aggregate_failures do
expect(subject[:file_type]).to eq(expected_file_type)
expect(subject[:architecture]).to eq(expected_architecture)
@@ -25,29 +27,79 @@ RSpec.describe Packages::Debian::ExtractMetadataService, feature_category: :pack
using RSpec::Parameterized::TableSyntax
- where(:case_name, :trait, :expected_file_type, :expected_architecture, :expected_fields) do
- 'with invalid' | :invalid | :unknown | nil | nil
- 'with source' | :source | :source | nil | nil
- 'with dsc' | :dsc | :dsc | nil | { 'Binary' => 'sample-dev, libsample0, sample-udeb' }
- 'with deb' | :deb | :deb | 'amd64' | { 'Multi-Arch' => 'same' }
- 'with udeb' | :udeb | :udeb | 'amd64' | { 'Package' => 'sample-udeb' }
- 'with buildinfo' | :buildinfo | :buildinfo | nil | { 'Architecture' => 'amd64 source', 'Build-Architecture' => 'amd64' }
- 'with changes' | :changes | :changes | nil | { 'Architecture' => 'source amd64', 'Binary' => 'libsample0 sample-dev sample-udeb' }
+ context 'with valid file types' do
+ where(:case_name, :trait, :expected_file_type, :expected_architecture, :expected_fields) do
+ 'with source' | :source | :source | nil | nil
+ 'with dsc' | :dsc | :dsc | nil | { 'Binary' => 'sample-dev, libsample0, sample-udeb, sample-ddeb' }
+ 'with deb' | :deb | :deb | 'amd64' | { 'Multi-Arch' => 'same' }
+ 'with udeb' | :udeb | :udeb | 'amd64' | { 'Package' => 'sample-udeb' }
+ 'with ddeb' | :ddeb | :ddeb | 'amd64' | { 'Package' => 'sample-ddeb' }
+ 'with buildinfo' | :buildinfo | :buildinfo | nil | { 'Architecture' => 'amd64 source',
+ 'Build-Architecture' => 'amd64' }
+ 'with changes' | :changes | :changes | nil | { 'Architecture' => 'source amd64',
+ 'Binary' => 'libsample0 sample-dev sample-udeb' }
+ end
+
+ with_them do
+ include_context 'with Debian package file', params[:trait] do
+ it_behaves_like 'Test Debian ExtractMetadata Service',
+ params[:expected_file_type],
+ params[:expected_architecture],
+ params[:expected_fields]
+ end
+ end
end
- with_them do
- include_context 'Debian ExtractMetadata Service', params[:trait] do
- it_behaves_like 'Test Debian ExtractMetadata Service',
- params[:expected_file_type],
- params[:expected_architecture],
- params[:expected_fields]
+ context 'with valid source extensions' do
+ where(:ext) do
+ %i[gz bz2 lzma xz]
+ end
+
+ with_them do
+ let(:package_file) do
+ create(:debian_package_file, :source, file_name: "myfile.tar.#{ext}",
+ file_fixture: 'spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz')
+ end
+
+ it_behaves_like 'Test Debian ExtractMetadata Service', :source
+ end
+ end
+
+ context 'with invalid source extensions' do
+ where(:ext) do
+ %i[gzip bzip2]
+ end
+
+ with_them do
+ let(:package_file) do
+ create(:debian_package_file, :source, file_name: "myfile.tar.#{ext}",
+ file_fixture: 'spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz')
+ end
+
+ it 'raises an error' do
+ expect do
+ subject
+ end.to raise_error(described_class::ExtractionError,
+ "unsupported file extension for file #{package_file.file_name}")
+ end
+ end
+ end
+
+ context 'with invalid file name' do
+ let(:package_file) { create(:debian_package_file, :invalid) }
+
+ it 'raises an error' do
+ expect do
+ subject
+ end.to raise_error(described_class::ExtractionError,
+ "unsupported file extension for file #{package_file.file_name}")
end
end
context 'with invalid package file' do
let(:package_file) { create(:conan_package_file) }
- it 'raise error' do
+ it 'raises an error' do
expect { subject }.to raise_error(described_class::ExtractionError, 'invalid package file')
end
end
diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb
index 6d179c791a3..27206b847e4 100644
--- a/spec/services/packages/debian/generate_distribution_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_service_spec.rb
@@ -3,20 +3,32 @@
require 'spec_helper'
RSpec.describe Packages::Debian::GenerateDistributionService, feature_category: :package_registry do
- describe '#execute' do
- subject { described_class.new(distribution).execute }
+ include_context 'with published Debian package'
- let(:subject2) { described_class.new(distribution).execute }
- let(:subject3) { described_class.new(distribution).execute }
+ let(:service) { described_class.new(distribution) }
- include_context 'with published Debian package'
+ [:project, :group].each do |container_type|
+ context "for #{container_type}" do
+ include_context 'with Debian distribution', container_type
- [:project, :group].each do |container_type|
- context "for #{container_type}" do
- include_context 'with Debian distribution', container_type
+ describe '#execute' do
+ subject { service.execute }
+
+ let(:subject2) { described_class.new(distribution).execute }
+ let(:subject3) { described_class.new(distribution).execute }
it_behaves_like 'Generate Debian Distribution and component files'
end
+
+ describe '#lease_key' do
+ subject { service.send(:lease_key) }
+
+ let(:prefix) { "packages:debian:generate_distribution_service:" }
+
+ it 'returns an unique key' do
+ is_expected.to eq "#{prefix}#{container_type}_distribution:#{distribution.id}"
+ end
+ end
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 35b7ead9209..624d4d95e5a 100644
--- a/spec/services/packages/debian/parse_debian822_service_spec.rb
+++ b/spec/services/packages/debian/parse_debian822_service_spec.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe Packages::Debian::ParseDebian822Service, feature_category: :package_registry do
@@ -76,14 +77,19 @@ RSpec.describe Packages::Debian::ParseDebian822Service, feature_category: :packa
'Multi-Arch' => 'same',
'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-Type' => 'udeb',
- 'Architecture' => 'any',
- 'Depends' => 'installed-base',
- 'Description' => 'Some mostly empty udeb'
- }
+ 'Package' => 'sample-udeb',
+ 'Package-Type' => 'udeb',
+ 'Architecture' => 'any',
+ 'Depends' => 'installed-base',
+ 'Description' => 'Some mostly empty udeb'
+ },
+ 'Package: sample-ddeb' => {
+ 'Package' => 'sample-ddeb',
+ 'Architecture' => 'any',
+ 'Description' => 'Some fake Ubuntu ddeb'
+ }
}
expect(subject.execute.to_s).to eq(expected.to_s)
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
index e3ed744377e..d2c05b678ea 100644
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ b/spec/services/packages/debian/process_changes_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Packages::Debian::ProcessChangesService, feature_category: :packa
expect { subject.execute }
.to change { Packages::Package.count }.from(1).to(2)
.and not_change { Packages::PackageFile.count }
- .and change { incoming.package_files.count }.from(7).to(0)
+ .and change { incoming.package_files.count }.from(8).to(0)
.and change { package_file.debian_file_metadatum&.reload&.file_type }.from('unknown').to('changes')
created_package = Packages::Package.last
diff --git a/spec/services/packages/debian/process_package_file_service_spec.rb b/spec/services/packages/debian/process_package_file_service_spec.rb
index caf29cfc4fa..2684b69785a 100644
--- a/spec/services/packages/debian/process_package_file_service_spec.rb
+++ b/spec/services/packages/debian/process_package_file_service_spec.rb
@@ -35,6 +35,7 @@ RSpec.describe Packages::Debian::ProcessPackageFileService, feature_category: :p
where(:case_name, :expected_file_type, :file_name, :component_name) do
'with a deb' | 'deb' | 'libsample0_1.2.3~alpha2_amd64.deb' | 'main'
'with an udeb' | 'udeb' | 'sample-udeb_1.2.3~alpha2_amd64.udeb' | 'contrib'
+ 'with an ddeb' | 'ddeb' | 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | 'main'
end
with_them do
@@ -67,9 +68,9 @@ RSpec.describe Packages::Debian::ProcessPackageFileService, feature_category: :p
.to receive(:perform_async).with(:project, distribution.id)
expect { subject.execute }
.to change(Packages::Package, :count).from(2).to(1)
- .and change(Packages::PackageFile, :count).from(14).to(8)
+ .and change(Packages::PackageFile, :count).from(16).to(9)
.and not_change(Packages::Debian::Publication, :count)
- .and change(package.package_files, :count).from(7).to(0)
+ .and change(package.package_files, :count).from(8).to(0)
.and change(package_file, :package).from(package).to(matching_package)
.and not_change(matching_package, :name)
.and not_change(matching_package, :version)
diff --git a/spec/services/packages/generic/create_package_file_service_spec.rb b/spec/services/packages/generic/create_package_file_service_spec.rb
index 9d6784b7721..08c4cbdbe11 100644
--- a/spec/services/packages/generic/create_package_file_service_spec.rb
+++ b/spec/services/packages/generic/create_package_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Generic::CreatePackageFileService do
+RSpec.describe Packages::Generic::CreatePackageFileService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:pipeline) { create(:ci_pipeline, user: user) }
diff --git a/spec/services/packages/generic/find_or_create_package_service_spec.rb b/spec/services/packages/generic/find_or_create_package_service_spec.rb
index 10ec917bc99..07054fe3651 100644
--- a/spec/services/packages/generic/find_or_create_package_service_spec.rb
+++ b/spec/services/packages/generic/find_or_create_package_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Generic::FindOrCreatePackageService do
+RSpec.describe Packages::Generic::FindOrCreatePackageService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:ci_build) { create(:ci_build, :running, user: user) }
diff --git a/spec/services/packages/go/create_package_service_spec.rb b/spec/services/packages/go/create_package_service_spec.rb
index 4ca1119fbaa..f552af81077 100644
--- a/spec/services/packages/go/create_package_service_spec.rb
+++ b/spec/services/packages/go/create_package_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Go::CreatePackageService do
+RSpec.describe Packages::Go::CreatePackageService, feature_category: :package_registry do
let_it_be(:project) { create :project_empty_repo, path: 'my-go-lib' }
let_it_be(:mod) { create :go_module, project: project }
diff --git a/spec/services/packages/go/sync_packages_service_spec.rb b/spec/services/packages/go/sync_packages_service_spec.rb
index 565b0f252ce..2881b6fdac9 100644
--- a/spec/services/packages/go/sync_packages_service_spec.rb
+++ b/spec/services/packages/go/sync_packages_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Go::SyncPackagesService do
+RSpec.describe Packages::Go::SyncPackagesService, feature_category: :package_registry do
include_context 'basic Go module'
let(:params) { { info: true, mod: true, zip: true } }
diff --git a/spec/services/packages/helm/extract_file_metadata_service_spec.rb b/spec/services/packages/helm/extract_file_metadata_service_spec.rb
index f4c61c12344..861d326d12a 100644
--- a/spec/services/packages/helm/extract_file_metadata_service_spec.rb
+++ b/spec/services/packages/helm/extract_file_metadata_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Helm::ExtractFileMetadataService do
+RSpec.describe Packages::Helm::ExtractFileMetadataService, feature_category: :package_registry do
let_it_be(:package_file) { create(:helm_package_file) }
let(:service) { described_class.new(package_file) }
diff --git a/spec/services/packages/helm/process_file_service_spec.rb b/spec/services/packages/helm/process_file_service_spec.rb
index 1be0153a4a5..a1f53e8756c 100644
--- a/spec/services/packages/helm/process_file_service_spec.rb
+++ b/spec/services/packages/helm/process_file_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Helm::ProcessFileService do
+RSpec.describe Packages::Helm::ProcessFileService, feature_category: :package_registry do
let(:package) { create(:helm_package, without_package_files: true, status: 'processing') }
let!(:package_file) { create(:helm_package_file, without_loaded_metadatum: true, package: package) }
let(:channel) { 'stable' }
diff --git a/spec/services/packages/mark_package_files_for_destruction_service_spec.rb b/spec/services/packages/mark_package_files_for_destruction_service_spec.rb
index 66534338003..a00a0b79854 100644
--- a/spec/services/packages/mark_package_files_for_destruction_service_spec.rb
+++ b/spec/services/packages/mark_package_files_for_destruction_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Packages::MarkPackageFilesForDestructionService, :aggregate_failures do
+RSpec.describe Packages::MarkPackageFilesForDestructionService, :aggregate_failures,
+ feature_category: :package_registry do
let(:service) { described_class.new(package_files) }
describe '#execute', :aggregate_failures do
diff --git a/spec/services/packages/mark_package_for_destruction_service_spec.rb b/spec/services/packages/mark_package_for_destruction_service_spec.rb
index 125ec53ad61..d65e62b84a6 100644
--- a/spec/services/packages/mark_package_for_destruction_service_spec.rb
+++ b/spec/services/packages/mark_package_for_destruction_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::MarkPackageForDestructionService do
+RSpec.describe Packages::MarkPackageForDestructionService, feature_category: :package_registry do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:package) { create(:npm_package) }
@@ -36,6 +36,12 @@ RSpec.describe Packages::MarkPackageForDestructionService do
end
it 'returns an error ServiceResponse' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ project_id: package.project_id,
+ package_id: package.id
+ )
+
response = service.execute
expect(package).not_to receive(:sync_maven_metadata)
diff --git a/spec/services/packages/mark_packages_for_destruction_service_spec.rb b/spec/services/packages/mark_packages_for_destruction_service_spec.rb
index 5c043b89de8..22278f9927d 100644
--- a/spec/services/packages/mark_packages_for_destruction_service_spec.rb
+++ b/spec/services/packages/mark_packages_for_destruction_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::MarkPackagesForDestructionService, :sidekiq_inline do
+RSpec.describe Packages::MarkPackagesForDestructionService, :sidekiq_inline, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:packages) { create_list(:npm_package, 3, project: project) }
@@ -76,6 +76,11 @@ RSpec.describe Packages::MarkPackagesForDestructionService, :sidekiq_inline do
it 'returns an error ServiceResponse' do
expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new)
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ package_ids: package_ids
+ )
+
expect { subject }.to not_change { ::Packages::Package.pending_destruction.count }
.and not_change { ::Packages::PackageFile.pending_destruction.count }
diff --git a/spec/services/packages/maven/create_package_service_spec.rb b/spec/services/packages/maven/create_package_service_spec.rb
index 11bf00c1399..2c528c3591e 100644
--- a/spec/services/packages/maven/create_package_service_spec.rb
+++ b/spec/services/packages/maven/create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Maven::CreatePackageService do
+RSpec.describe Packages::Maven::CreatePackageService, feature_category: :package_registry do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:app_name) { 'my-app' }
diff --git a/spec/services/packages/maven/find_or_create_package_service_spec.rb b/spec/services/packages/maven/find_or_create_package_service_spec.rb
index cca074e2fa6..8b84d2541eb 100644
--- a/spec/services/packages/maven/find_or_create_package_service_spec.rb
+++ b/spec/services/packages/maven/find_or_create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Maven::FindOrCreatePackageService do
+RSpec.describe Packages::Maven::FindOrCreatePackageService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@@ -44,6 +44,15 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
end
end
+ shared_examples 'returning an error' do |with_message: ''|
+ it { expect { subject }.not_to change { project.package_files.count } }
+
+ it 'returns an error', :aggregate_failures do
+ expect(subject.payload).to be_empty
+ expect(subject.errors).to include(with_message)
+ end
+ end
+
context 'path with version' do
# Note that "path with version" and "file type maven metadata xml" only exists for snapshot versions
# In other words, we will never have an metadata xml upload on a path with version for a non snapshot version
@@ -128,11 +137,19 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
let!(:existing_package) { create(:maven_package, name: path, version: version, project: project) }
- it { expect { subject }.not_to change { project.package_files.count } }
+ let(:existing_file_name) { file_name }
+ let(:jar_file) { existing_package.package_files.with_file_name_like('%.jar').first }
- it 'returns an error', :aggregate_failures do
- expect(subject.payload).to be_empty
- expect(subject.errors).to include('Duplicate package is not allowed')
+ before do
+ jar_file.update_column(:file_name, existing_file_name)
+ end
+
+ it_behaves_like 'returning an error', with_message: 'Duplicate package is not allowed'
+
+ context 'for a SNAPSHOT version' do
+ let(:version) { '1.0.0-SNAPSHOT' }
+
+ it_behaves_like 'returning an error', with_message: 'Duplicate package is not allowed'
end
context 'when uploading to the versionless package which contains metadata about all versions' do
@@ -144,8 +161,7 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
context 'when uploading different non-duplicate files to the same package' do
before do
- package_file = existing_package.package_files.find_by(file_name: 'my-app-1.0-20180724.124855-1.jar')
- package_file.destroy!
+ jar_file.destroy!
end
it_behaves_like 'reuse existing package'
@@ -166,6 +182,27 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do
it_behaves_like 'reuse existing package'
end
+
+ context 'when uploading a similar package file name with a classifier' do
+ let(:existing_file_name) { 'test.jar' }
+ let(:file_name) { 'test-javadoc.jar' }
+
+ it_behaves_like 'reuse existing package'
+
+ context 'for a SNAPSHOT version' do
+ let(:version) { '1.0.0-SNAPSHOT' }
+ let(:existing_file_name) { 'test-1.0-20230303.163304-1.jar' }
+ let(:file_name) { 'test-1.0-20230303.163304-1-javadoc.jar' }
+
+ it_behaves_like 'reuse existing package'
+ end
+ end
+ end
+
+ context 'with a very large file name' do
+ let(:params) { super().merge(file_name: 'a' * (described_class::MAX_FILE_NAME_LENGTH + 1)) }
+
+ it_behaves_like 'returning an error', with_message: 'File name is too long'
end
end
end
diff --git a/spec/services/packages/maven/metadata/append_package_file_service_spec.rb b/spec/services/packages/maven/metadata/append_package_file_service_spec.rb
index f3a90d31158..f65029f7b64 100644
--- a/spec/services/packages/maven/metadata/append_package_file_service_spec.rb
+++ b/spec/services/packages/maven/metadata/append_package_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Packages::Maven::Metadata::AppendPackageFileService do
+RSpec.describe ::Packages::Maven::Metadata::AppendPackageFileService, feature_category: :package_registry do
let_it_be(:package) { create(:maven_package, version: nil) }
let(:service) { described_class.new(package: package, metadata_content: content) }
diff --git a/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb b/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb
index 6fc1087940d..d0ef037b2d9 100644
--- a/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb
+++ b/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Packages::Maven::Metadata::CreatePluginsXmlService do
+RSpec.describe ::Packages::Maven::Metadata::CreatePluginsXmlService, feature_category: :package_registry do
let_it_be(:group_id) { 'my/test' }
let_it_be(:package) { create(:maven_package, name: group_id, version: nil) }
diff --git a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
index 70c2bbad87a..6ae84b5df4e 100644
--- a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
+++ b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Packages::Maven::Metadata::CreateVersionsXmlService do
+RSpec.describe ::Packages::Maven::Metadata::CreateVersionsXmlService, feature_category: :package_registry do
let_it_be(:package) { create(:maven_package, version: nil) }
let(:versions_in_database) { %w[1.3 2.0-SNAPSHOT 1.6 1.4 1.5-SNAPSHOT] }
diff --git a/spec/services/packages/maven/metadata/sync_service_spec.rb b/spec/services/packages/maven/metadata/sync_service_spec.rb
index 9a704d749b3..eaed54d959b 100644
--- a/spec/services/packages/maven/metadata/sync_service_spec.rb
+++ b/spec/services/packages/maven/metadata/sync_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Packages::Maven::Metadata::SyncService do
+RSpec.describe ::Packages::Maven::Metadata::SyncService, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index ef8cdf2e8ab..70c79dae437 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Npm::CreatePackageService do
+RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_registry do
let(:namespace) { create(:namespace) }
let(:project) { create(:project, namespace: namespace) }
let(:user) { create(:user) }
diff --git a/spec/services/packages/npm/create_tag_service_spec.rb b/spec/services/packages/npm/create_tag_service_spec.rb
index a4b07bf97cc..682effc9f4f 100644
--- a/spec/services/packages/npm/create_tag_service_spec.rb
+++ b/spec/services/packages/npm/create_tag_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Npm::CreateTagService do
+RSpec.describe Packages::Npm::CreateTagService, feature_category: :package_registry do
let(:package) { create(:npm_package) }
let(:tag_name) { 'test-tag' }
diff --git a/spec/services/packages/nuget/create_dependency_service_spec.rb b/spec/services/packages/nuget/create_dependency_service_spec.rb
index 268c8837e25..10daec8b871 100644
--- a/spec/services/packages/nuget/create_dependency_service_spec.rb
+++ b/spec/services/packages/nuget/create_dependency_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Nuget::CreateDependencyService do
+RSpec.describe Packages::Nuget::CreateDependencyService, feature_category: :package_registry do
let_it_be(:package, reload: true) { create(:nuget_package) }
describe '#execute' do
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 12bab30b4a7..9177a5379d9 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::MetadataExtractionService do
+RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :package_registry do
let_it_be(:package_file) { create(:nuget_package).package_files.first }
let(:service) { described_class.new(package_file.id) }
diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb
index 66c91487a8f..b5f32c9b727 100644
--- a/spec/services/packages/nuget/search_service_spec.rb
+++ b/spec/services/packages/nuget/search_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::SearchService do
+RSpec.describe Packages::Nuget::SearchService, feature_category: :package_registry do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
diff --git a/spec/services/packages/nuget/sync_metadatum_service_spec.rb b/spec/services/packages/nuget/sync_metadatum_service_spec.rb
index 32093c48b76..ae07f312fcc 100644
--- a/spec/services/packages/nuget/sync_metadatum_service_spec.rb
+++ b/spec/services/packages/nuget/sync_metadatum_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::SyncMetadatumService do
+RSpec.describe Packages::Nuget::SyncMetadatumService, feature_category: :package_registry do
let_it_be(:package, reload: true) { create(:nuget_package) }
let_it_be(:metadata) do
{
diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
index 6a4dbeb10dc..3a13f2ff902 100644
--- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
+++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do
+RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state, feature_category: :package_registry do
include ExclusiveLeaseHelpers
let!(:package) { create(:nuget_package, :processing, :with_symbol_package) }
diff --git a/spec/services/packages/pypi/create_package_service_spec.rb b/spec/services/packages/pypi/create_package_service_spec.rb
index 6794ab4d9d6..0d278e32e89 100644
--- a/spec/services/packages/pypi/create_package_service_spec.rb
+++ b/spec/services/packages/pypi/create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Pypi::CreatePackageService, :aggregate_failures do
+RSpec.describe Packages::Pypi::CreatePackageService, :aggregate_failures, feature_category: :package_registry do
include PackagesManagerApiSpecHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/services/packages/remove_tag_service_spec.rb b/spec/services/packages/remove_tag_service_spec.rb
index 084635824e5..4ad478d487a 100644
--- a/spec/services/packages/remove_tag_service_spec.rb
+++ b/spec/services/packages/remove_tag_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::RemoveTagService do
+RSpec.describe Packages::RemoveTagService, feature_category: :package_registry do
let!(:package_tag) { create(:packages_tag) }
describe '#execute' do
diff --git a/spec/services/packages/rpm/parse_package_service_spec.rb b/spec/services/packages/rpm/parse_package_service_spec.rb
index f330587bfa0..80907d8f43f 100644
--- a/spec/services/packages/rpm/parse_package_service_spec.rb
+++ b/spec/services/packages/rpm/parse_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::ParsePackageService do
+RSpec.describe Packages::Rpm::ParsePackageService, feature_category: :package_registry do
let(:package_file) { File.open('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
describe 'dynamic private methods' do
diff --git a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
index d93d6ab9fcb..e0d9e192d97 100644
--- a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXmlService do
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXmlService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(data).execute }
diff --git a/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
index 201f9e67ce9..e81a436e006 100644
--- a/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXmlService do
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXmlService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(data).execute }
diff --git a/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
index 9bbfa5c9863..1e534782841 100644
--- a/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXmlService do
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXmlService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(data).execute }
diff --git a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
index cf28301fa2c..99fcf0fabbf 100644
--- a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXmlService do
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXmlService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(data).execute }
diff --git a/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
index e351392ba1c..68a3ac7d82f 100644
--- a/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::UpdateXmlService do
+RSpec.describe Packages::Rpm::RepositoryMetadata::UpdateXmlService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(filename: filename, xml: xml, data: data).execute }
diff --git a/spec/services/packages/rubygems/create_dependencies_service_spec.rb b/spec/services/packages/rubygems/create_dependencies_service_spec.rb
index b6e12b1cc61..d689bae96ff 100644
--- a/spec/services/packages/rubygems/create_dependencies_service_spec.rb
+++ b/spec/services/packages/rubygems/create_dependencies_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rubygems::CreateDependenciesService do
+RSpec.describe Packages::Rubygems::CreateDependenciesService, feature_category: :package_registry do
include RubygemsHelpers
let_it_be(:package) { create(:rubygems_package) }
diff --git a/spec/services/packages/rubygems/create_gemspec_service_spec.rb b/spec/services/packages/rubygems/create_gemspec_service_spec.rb
index 839fb4d955a..17890100b93 100644
--- a/spec/services/packages/rubygems/create_gemspec_service_spec.rb
+++ b/spec/services/packages/rubygems/create_gemspec_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rubygems::CreateGemspecService do
+RSpec.describe Packages::Rubygems::CreateGemspecService, feature_category: :package_registry do
include RubygemsHelpers
let_it_be(:package_file) { create(:package_file, :gem) }
diff --git a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb
index bb84e0cd361..9a72c51e99c 100644
--- a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb
+++ b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rubygems::DependencyResolverService do
+RSpec.describe Packages::Rubygems::DependencyResolverService, feature_category: :package_registry do
let_it_be(:project) { create(:project, :private) }
let_it_be(:package) { create(:package, project: project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/packages/rubygems/metadata_extraction_service_spec.rb b/spec/services/packages/rubygems/metadata_extraction_service_spec.rb
index bbd5b6f3d59..87d63eff311 100644
--- a/spec/services/packages/rubygems/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/rubygems/metadata_extraction_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
require 'rubygems/package'
-RSpec.describe Packages::Rubygems::MetadataExtractionService do
+RSpec.describe Packages::Rubygems::MetadataExtractionService, feature_category: :package_registry do
include RubygemsHelpers
let_it_be(:package) { create(:rubygems_package) }
diff --git a/spec/services/packages/rubygems/process_gem_service_spec.rb b/spec/services/packages/rubygems/process_gem_service_spec.rb
index caff338ef53..a1b4eae9655 100644
--- a/spec/services/packages/rubygems/process_gem_service_spec.rb
+++ b/spec/services/packages/rubygems/process_gem_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Rubygems::ProcessGemService do
+RSpec.describe Packages::Rubygems::ProcessGemService, feature_category: :package_registry do
include ExclusiveLeaseHelpers
include RubygemsHelpers
diff --git a/spec/services/packages/terraform_module/create_package_service_spec.rb b/spec/services/packages/terraform_module/create_package_service_spec.rb
index f73b5682835..3355dfcf5ec 100644
--- a/spec/services/packages/terraform_module/create_package_service_spec.rb
+++ b/spec/services/packages/terraform_module/create_package_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::TerraformModule::CreatePackageService do
+RSpec.describe Packages::TerraformModule::CreatePackageService, feature_category: :package_registry do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/packages/update_package_file_service_spec.rb b/spec/services/packages/update_package_file_service_spec.rb
index d988049c43a..5d081059105 100644
--- a/spec/services/packages/update_package_file_service_spec.rb
+++ b/spec/services/packages/update_package_file_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::UpdatePackageFileService do
+RSpec.describe Packages::UpdatePackageFileService, feature_category: :package_registry do
let_it_be(:another_package) { create(:package) }
let_it_be(:old_file_name) { 'old_file_name.txt' }
let_it_be(:new_file_name) { 'new_file_name.txt' }
diff --git a/spec/services/packages/update_tags_service_spec.rb b/spec/services/packages/update_tags_service_spec.rb
index c4256699c94..d8f572fff32 100644
--- a/spec/services/packages/update_tags_service_spec.rb
+++ b/spec/services/packages/update_tags_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::UpdateTagsService do
+RSpec.describe Packages::UpdateTagsService, feature_category: :package_registry do
let_it_be(:package, reload: true) { create(:nuget_package) }
let(:tags) { %w(test-tag tag1 tag2 tag3) }
diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb
index 8b9e72ac9b1..590378af22b 100644
--- a/spec/services/pages/delete_service_spec.rb
+++ b/spec/services/pages/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Pages::DeleteService do
+RSpec.describe Pages::DeleteService, feature_category: :pages do
let_it_be(:admin) { create(:admin) }
let(:project) { create(:project, path: "my.project") }
diff --git a/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb b/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
index 177467aac85..b18f62c1c28 100644
--- a/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
+++ b/spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
+RSpec.describe Pages::MigrateLegacyStorageToDeploymentService, feature_category: :pages do
let(:project) { create(:project, :repository) }
let(:service) { described_class.new(project) }
diff --git a/spec/services/pages/zip_directory_service_spec.rb b/spec/services/pages/zip_directory_service_spec.rb
index 00fe75dbbfd..4917bc65a02 100644
--- a/spec/services/pages/zip_directory_service_spec.rb
+++ b/spec/services/pages/zip_directory_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Pages::ZipDirectoryService do
+RSpec.describe Pages::ZipDirectoryService, feature_category: :pages do
around do |example|
Dir.mktmpdir do |dir|
@work_dir = dir
diff --git a/spec/services/pages_domains/create_acme_order_service_spec.rb b/spec/services/pages_domains/create_acme_order_service_spec.rb
index 35b2cc56973..97534d52c67 100644
--- a/spec/services/pages_domains/create_acme_order_service_spec.rb
+++ b/spec/services/pages_domains/create_acme_order_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomains::CreateAcmeOrderService do
+RSpec.describe PagesDomains::CreateAcmeOrderService, feature_category: :pages do
include LetsEncryptHelpers
let(:pages_domain) { create(:pages_domain) }
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index ecb445fa441..2377fbcf003 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService do
+RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService, feature_category: :pages do
include LetsEncryptHelpers
let(:pages_domain) { create(:pages_domain, :without_certificate, :without_key) }
diff --git a/spec/services/personal_access_tokens/create_service_spec.rb b/spec/services/personal_access_tokens/create_service_spec.rb
index b8a4c8f30d2..d80be5cccce 100644
--- a/spec/services/personal_access_tokens/create_service_spec.rb
+++ b/spec/services/personal_access_tokens/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokens::CreateService do
+RSpec.describe PersonalAccessTokens::CreateService, feature_category: :system_access do
shared_examples_for 'a successfully created token' do
it 'creates personal access token record' do
expect(subject.success?).to be true
@@ -40,7 +40,7 @@ RSpec.describe PersonalAccessTokens::CreateService do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let(:params) { { name: 'Test token', impersonation: false, scopes: [:api], expires_at: Date.today + 1.month } }
- let(:service) { described_class.new(current_user: current_user, target_user: user, params: params) }
+ let(:service) { described_class.new(current_user: current_user, target_user: user, params: params, concatenate_errors: false) }
let(:token) { subject.payload[:personal_access_token] }
context 'when current_user is an administrator' do
@@ -66,5 +66,21 @@ RSpec.describe PersonalAccessTokens::CreateService do
it_behaves_like 'a successfully created token'
end
end
+
+ context 'when invalid scope' do
+ let(:params) { { name: 'Test token', impersonation: false, scopes: [:no_valid], expires_at: Date.today + 1.month } }
+
+ context 'when concatenate_errors: true' do
+ let(:service) { described_class.new(current_user: user, target_user: user, params: params) }
+
+ it { expect(subject.message).to be_an_instance_of(String) }
+ end
+
+ context 'when concatenate_errors: false' do
+ let(:service) { described_class.new(current_user: user, target_user: user, params: params, concatenate_errors: false) }
+
+ it { expect(subject.message).to be_an_instance_of(Array) }
+ end
+ end
end
end
diff --git a/spec/services/personal_access_tokens/last_used_service_spec.rb b/spec/services/personal_access_tokens/last_used_service_spec.rb
index 6fc74e27dd9..20eabc20338 100644
--- a/spec/services/personal_access_tokens/last_used_service_spec.rb
+++ b/spec/services/personal_access_tokens/last_used_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokens::LastUsedService do
+RSpec.describe PersonalAccessTokens::LastUsedService, feature_category: :system_access do
describe '#execute' do
subject { described_class.new(personal_access_token).execute }
diff --git a/spec/services/personal_access_tokens/revoke_service_spec.rb b/spec/services/personal_access_tokens/revoke_service_spec.rb
index a9b4df9749f..4c5d106660a 100644
--- a/spec/services/personal_access_tokens/revoke_service_spec.rb
+++ b/spec/services/personal_access_tokens/revoke_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokens::RevokeService do
+RSpec.describe PersonalAccessTokens::RevokeService, feature_category: :system_access do
shared_examples_for 'a successfully revoked token' do
it { expect(subject.success?).to be true }
it { expect(service.token.revoked?).to be true }
diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb
index aa955b3445b..13bd103003f 100644
--- a/spec/services/post_receive_service_spec.rb
+++ b/spec/services/post_receive_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PostReceiveService do
+RSpec.describe PostReceiveService, feature_category: :team_planning do
include GitlabShellHelpers
include Gitlab::Routing
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index d1bc10cfd28..97e31ec2cd3 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PreviewMarkdownService do
+RSpec.describe PreviewMarkdownService, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/product_analytics/build_activity_graph_service_spec.rb b/spec/services/product_analytics/build_activity_graph_service_spec.rb
index e303656da34..cd1bc42e156 100644
--- a/spec/services/product_analytics/build_activity_graph_service_spec.rb
+++ b/spec/services/product_analytics/build_activity_graph_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProductAnalytics::BuildActivityGraphService do
+RSpec.describe ProductAnalytics::BuildActivityGraphService, feature_category: :product_analytics do
let_it_be(:project) { create(:project) }
let_it_be(:time_now) { Time.zone.now }
let_it_be(:time_ago) { Time.zone.now - 5.days }
diff --git a/spec/services/product_analytics/build_graph_service_spec.rb b/spec/services/product_analytics/build_graph_service_spec.rb
index 933a2bfee92..ee0e2190501 100644
--- a/spec/services/product_analytics/build_graph_service_spec.rb
+++ b/spec/services/product_analytics/build_graph_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProductAnalytics::BuildGraphService do
+RSpec.describe ProductAnalytics::BuildGraphService, feature_category: :product_analytics do
let_it_be(:project) { create(:project) }
let_it_be(:events) do
diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb
index 72bb0adbf56..3097d6d1498 100644
--- a/spec/services/projects/after_rename_service_spec.rb
+++ b/spec/services/projects/after_rename_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AfterRenameService do
+RSpec.describe Projects::AfterRenameService, feature_category: :projects do
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
let!(:path_before_rename) { project.path }
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index aa2ef39bf98..8cd9b5d3e00 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Alerting::NotifyService do
+RSpec.describe Projects::Alerting::NotifyService, feature_category: :projects do
let_it_be_with_reload(:project) { create(:project) }
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
diff --git a/spec/services/projects/all_issues_count_service_spec.rb b/spec/services/projects/all_issues_count_service_spec.rb
index d7e35991940..e8e08a25c45 100644
--- a/spec/services/projects/all_issues_count_service_spec.rb
+++ b/spec/services/projects/all_issues_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AllIssuesCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Projects::AllIssuesCountService, :use_clean_rails_memory_store_caching, feature_category: :projects do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:banned_user) { create(:user, :banned) }
diff --git a/spec/services/projects/all_merge_requests_count_service_spec.rb b/spec/services/projects/all_merge_requests_count_service_spec.rb
index 13954d688aa..dc7038611ed 100644
--- a/spec/services/projects/all_merge_requests_count_service_spec.rb
+++ b/spec/services/projects/all_merge_requests_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AllMergeRequestsCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Projects::AllMergeRequestsCountService, :use_clean_rails_memory_store_caching, feature_category: :projects do
let_it_be(:project) { create(:project) }
subject { described_class.new(project) }
diff --git a/spec/services/projects/android_target_platform_detector_service_spec.rb b/spec/services/projects/android_target_platform_detector_service_spec.rb
index 74fd320bb48..d5feef4ec3d 100644
--- a/spec/services/projects/android_target_platform_detector_service_spec.rb
+++ b/spec/services/projects/android_target_platform_detector_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AndroidTargetPlatformDetectorService do
+RSpec.describe Projects::AndroidTargetPlatformDetectorService, feature_category: :projects do
let_it_be(:project) { build(:project) }
subject { described_class.new(project).execute }
diff --git a/spec/services/projects/apple_target_platform_detector_service_spec.rb b/spec/services/projects/apple_target_platform_detector_service_spec.rb
index 6391161824c..787faaa0f79 100644
--- a/spec/services/projects/apple_target_platform_detector_service_spec.rb
+++ b/spec/services/projects/apple_target_platform_detector_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AppleTargetPlatformDetectorService do
+RSpec.describe Projects::AppleTargetPlatformDetectorService, feature_category: :projects do
let_it_be(:project) { build(:project) }
subject { described_class.new(project).execute }
diff --git a/spec/services/projects/auto_devops/disable_service_spec.rb b/spec/services/projects/auto_devops/disable_service_spec.rb
index 1f161990fb2..fd70362a53f 100644
--- a/spec/services/projects/auto_devops/disable_service_spec.rb
+++ b/spec/services/projects/auto_devops/disable_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::AutoDevops::DisableService, '#execute' do
+RSpec.describe Projects::AutoDevops::DisableService, '#execute', feature_category: :auto_devops do
let(:project) { create(:project, :repository, :auto_devops) }
let(:auto_devops) { project.auto_devops }
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index bc95a1f3c8b..9d3075874a2 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AutocompleteService do
+RSpec.describe Projects::AutocompleteService, feature_category: :projects do
describe '#issues' do
describe 'confidential issues' do
let(:author) { create(:user) }
diff --git a/spec/services/projects/batch_open_issues_count_service_spec.rb b/spec/services/projects/batch_open_issues_count_service_spec.rb
index 89a4abbf9c9..d29115a697f 100644
--- a/spec/services/projects/batch_open_issues_count_service_spec.rb
+++ b/spec/services/projects/batch_open_issues_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::BatchOpenIssuesCountService do
+RSpec.describe Projects::BatchOpenIssuesCountService, feature_category: :projects do
let!(:project_1) { create(:project) }
let!(:project_2) { create(:project) }
diff --git a/spec/services/projects/batch_open_merge_requests_count_service_spec.rb b/spec/services/projects/batch_open_merge_requests_count_service_spec.rb
new file mode 100644
index 00000000000..96fc6c5e9dd
--- /dev/null
+++ b/spec/services/projects/batch_open_merge_requests_count_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::BatchOpenMergeRequestsCountService, feature_category: :code_review_workflow do
+ subject { described_class.new([project_1, project_2]) }
+
+ let_it_be(:project_1) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+
+ describe '#refresh_cache_and_retrieve_data', :use_clean_rails_memory_store_caching do
+ before do
+ create(:merge_request, source_project: project_1, target_project: project_1)
+ create(:merge_request, source_project: project_2, target_project: project_2)
+ end
+
+ it 'refreshes cache keys correctly when cache is clean', :aggregate_failures do
+ subject.refresh_cache_and_retrieve_data
+
+ expect(Rails.cache.read(get_cache_key(subject, project_1))).to eq(1)
+ expect(Rails.cache.read(get_cache_key(subject, project_2))).to eq(1)
+
+ expect { subject.refresh_cache_and_retrieve_data }.not_to exceed_query_limit(0)
+ end
+ end
+
+ def get_cache_key(subject, project)
+ subject.count_service
+ .new(project)
+ .cache_key
+ end
+end
diff --git a/spec/services/projects/blame_service_spec.rb b/spec/services/projects/blame_service_spec.rb
index 52b0ed3412d..e3df69b3b7b 100644
--- a/spec/services/projects/blame_service_spec.rb
+++ b/spec/services/projects/blame_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::BlameService, :aggregate_failures do
+RSpec.describe Projects::BlameService, :aggregate_failures, feature_category: :source_code_management do
subject(:service) { described_class.new(blob, commit, params) }
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/projects/branches_by_mode_service_spec.rb b/spec/services/projects/branches_by_mode_service_spec.rb
index 9a63563b37b..bfe76b34310 100644
--- a/spec/services/projects/branches_by_mode_service_spec.rb
+++ b/spec/services/projects/branches_by_mode_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::BranchesByModeService do
+RSpec.describe Projects::BranchesByModeService, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb
index f2c052d9397..533a09f7bc7 100644
--- a/spec/services/projects/cleanup_service_spec.rb
+++ b/spec/services/projects/cleanup_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::CleanupService do
+RSpec.describe Projects::CleanupService, feature_category: :source_code_management do
subject(:service) { described_class.new(project) }
describe '.enqueue' do
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 8311c4e4d9b..b8ad63d9b8a 100644
--- a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::CleanupTagsService do
+RSpec.describe Projects::ContainerRepository::CleanupTagsService, feature_category: :container_registry do
let_it_be_with_reload(:container_repository) { create(:container_repository) }
let_it_be(:user) { container_repository.project.owner }
@@ -77,7 +77,7 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService do
context 'with a migrated repository' do
before do
- container_repository.update_column(:migration_state, :import_done)
+ allow(container_repository).to receive(:migrated?).and_return(true)
end
context 'supporting the gitlab api' do
@@ -99,8 +99,7 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService do
context 'with a non migrated repository' do
before do
- container_repository.update_column(:migration_state, :default)
- container_repository.update!(created_at: ContainerRepository::MIGRATION_PHASE_1_ENDED_AT - 1.week)
+ allow(container_repository).to receive(:migrated?).and_return(false)
end
it_behaves_like 'calling service', ::Projects::ContainerRepository::ThirdParty::CleanupTagsService, extra_log_data: { third_party_cleanup_tags_service: true }
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
index 9e6849aa514..5b67d614dfb 100644
--- a/spec/services/projects/container_repository/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::DeleteTagsService do
+RSpec.describe Projects::ContainerRepository::DeleteTagsService, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include_context 'container repository delete tags service shared context'
diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb
index fed1d13daa5..a142360f99d 100644
--- a/spec/services/projects/container_repository/destroy_service_spec.rb
+++ b/spec/services/projects/container_repository/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::DestroyService do
+RSpec.describe Projects::ContainerRepository::DestroyService, feature_category: :container_registry do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:params) { {} }
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
index b06a5709bd5..416a3ed9782 100644
--- a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
+RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include_context 'for a cleanup tags service'
@@ -11,11 +11,13 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
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(:repository) { create(:container_repository, :root, 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
+ allow(repository).to receive(:migrated?).and_return(true)
+
project.add_maintainer(user) if user
stub_container_registry_config(enabled: true)
@@ -147,6 +149,20 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do
it_behaves_like 'when running a container_expiration_policy',
delete_expectations: [%w[Ba Bb C]]
end
+
+ context 'with no tags page' do
+ let(:tags_page_size) { 1000 }
+ let(:deleted) { [] }
+ let(:params) { {} }
+
+ before do
+ allow(repository.gitlab_api_client)
+ .to receive(:tags)
+ .and_return({})
+ end
+
+ it { is_expected.to eq(expected_service_response(status: :success, deleted: [], original_size: 0)) }
+ end
end
private
diff --git a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
index f03912dba80..c4e6c7f4a11 100644
--- a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
+RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService, feature_category: :container_registry do
include_context 'container repository delete tags service shared context'
let(:service) { described_class.new(repository, tags) }
diff --git a/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb
index 7227834b131..d9b30428fb5 100644
--- a/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::ThirdParty::CleanupTagsService, :clean_gitlab_redis_cache do
+RSpec.describe Projects::ContainerRepository::ThirdParty::CleanupTagsService, :clean_gitlab_redis_cache, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include_context 'for a cleanup tags service'
diff --git a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
index 4de36452684..0c297b6e1f7 100644
--- a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService do
+RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService, feature_category: :container_registry do
include_context 'container repository delete tags service shared context'
let(:service) { described_class.new(repository, tags) }
diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb
index 11b2b57a277..71940fa396e 100644
--- a/spec/services/projects/count_service_spec.rb
+++ b/spec/services/projects/count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::CountService do
+RSpec.describe Projects::CountService, feature_category: :projects do
let(:project) { build(:project, id: 1) }
let(:service) { described_class.new(project) }
diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
index fba6225b87a..a3fdb258f75 100644
--- a/spec/services/projects/create_from_template_service_spec.rb
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::CreateFromTemplateService do
+RSpec.describe Projects::CreateFromTemplateService, feature_category: :projects do
let(:user) { create(:user) }
let(:template_name) { 'rails' }
let(:project_params) do
diff --git a/spec/services/projects/deploy_tokens/create_service_spec.rb b/spec/services/projects/deploy_tokens/create_service_spec.rb
index 831dbc06588..96458a51fb4 100644
--- a/spec/services/projects/deploy_tokens/create_service_spec.rb
+++ b/spec/services/projects/deploy_tokens/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::DeployTokens::CreateService do
+RSpec.describe Projects::DeployTokens::CreateService, feature_category: :continuous_delivery do
it_behaves_like 'a deploy token creation service' do
let(:entity) { create(:project) }
let(:deploy_token_class) { ProjectDeployToken }
diff --git a/spec/services/projects/deploy_tokens/destroy_service_spec.rb b/spec/services/projects/deploy_tokens/destroy_service_spec.rb
index edb2345aa6c..3d0323c60ba 100644
--- a/spec/services/projects/deploy_tokens/destroy_service_spec.rb
+++ b/spec/services/projects/deploy_tokens/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::DeployTokens::DestroyService do
+RSpec.describe Projects::DeployTokens::DestroyService, feature_category: :continuous_delivery do
it_behaves_like 'a deploy token deletion service' do
let_it_be(:entity) { create(:project) }
let_it_be(:deploy_token_class) { ProjectDeployToken }
diff --git a/spec/services/projects/detect_repository_languages_service_spec.rb b/spec/services/projects/detect_repository_languages_service_spec.rb
index cf4c7a5024d..5759f8128d0 100644
--- a/spec/services/projects/detect_repository_languages_service_spec.rb
+++ b/spec/services/projects/detect_repository_languages_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_state do
+RSpec.describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_state, feature_category: :projects do
let_it_be(:project, reload: true) { create(:project, :repository) }
subject { described_class.new(project) }
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index f158b11a9fa..52bdbefe01a 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::DownloadService do
+RSpec.describe Projects::DownloadService, feature_category: :projects do
describe 'File service' do
before do
@user = create(:user)
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
index c0b3992037e..59c76a96d07 100644
--- a/spec/services/projects/enable_deploy_key_service_spec.rb
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::EnableDeployKeyService do
+RSpec.describe Projects::EnableDeployKeyService, feature_category: :continuous_delivery do
let(:deploy_key) { create(:deploy_key, public: true) }
let(:project) { create(:project) }
let(:user) { project.creator }
diff --git a/spec/services/projects/fetch_statistics_increment_service_spec.rb b/spec/services/projects/fetch_statistics_increment_service_spec.rb
index 16121a42c39..9e24e68fa98 100644
--- a/spec/services/projects/fetch_statistics_increment_service_spec.rb
+++ b/spec/services/projects/fetch_statistics_increment_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
module Projects
- RSpec.describe FetchStatisticsIncrementService do
+ RSpec.describe FetchStatisticsIncrementService, feature_category: :projects do
let(:project) { create(:project) }
describe '#execute' do
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 48756cf774b..8f42f5c8f87 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ForkService do
+RSpec.describe Projects::ForkService, feature_category: :source_code_management do
include ProjectForksHelper
shared_examples 'forks count cache refresh' do
diff --git a/spec/services/projects/forks/sync_service_spec.rb b/spec/services/projects/forks/sync_service_spec.rb
new file mode 100644
index 00000000000..aeb53992ed4
--- /dev/null
+++ b/spec/services/projects/forks/sync_service_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Forks::SyncService, feature_category: :source_code_management do
+ include ProjectForksHelper
+ include RepoHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:source_project) { create(:project, :repository, :public) }
+ let_it_be(:project) { fork_project(source_project, user, { repository: true }) }
+
+ let(:fork_branch) { project.default_branch }
+ let(:service) { described_class.new(project, user, fork_branch) }
+
+ def details
+ Projects::Forks::Details.new(project, fork_branch)
+ end
+
+ def expect_to_cancel_exclusive_lease
+ expect(Gitlab::ExclusiveLease).to receive(:cancel)
+ end
+
+ describe '#execute' do
+ context 'when fork is up-to-date with the upstream' do
+ it 'does not perform merge' do
+ expect_to_cancel_exclusive_lease
+ expect(project.repository).not_to receive(:merge_to_branch)
+ expect(project.repository).not_to receive(:ff_merge)
+
+ expect(service.execute).to be_success
+ end
+ end
+
+ context 'when fork is behind the upstream' do
+ let_it_be(:base_commit) { source_project.commit.sha }
+
+ before_all do
+ source_project.repository.commit_files(
+ user,
+ branch_name: source_project.repository.root_ref, message: 'Commit to root ref',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'One more' }]
+ )
+
+ source_project.repository.commit_files(
+ user,
+ branch_name: source_project.repository.root_ref, message: 'Another commit to root ref',
+ actions: [{ action: :create, file_path: 'encoding/NEW-CHANGELOG', content: 'One more time' }]
+ )
+ end
+
+ before do
+ project.repository.create_branch(fork_branch, base_commit)
+ end
+
+ context 'when fork is not ahead of the upstream' do
+ let(:fork_branch) { 'fork-without-new-commits' }
+
+ it 'updates the fork using ff merge' do
+ expect_to_cancel_exclusive_lease
+ expect(project.commit(fork_branch).sha).to eq(base_commit)
+ expect(project.repository).to receive(:ff_merge)
+ .with(user, source_project.commit.sha, fork_branch, target_sha: base_commit)
+ .and_call_original
+
+ expect do
+ expect(service.execute).to be_success
+ end.to change { details.counts }.from({ ahead: 0, behind: 2 }).to({ ahead: 0, behind: 0 })
+ end
+ end
+
+ context 'when fork is ahead of the upstream' do
+ context 'and has conflicts with the upstream', :use_clean_rails_redis_caching do
+ let(:fork_branch) { 'fork-with-conflicts' }
+
+ it 'returns an error' do
+ project.repository.commit_files(
+ user,
+ branch_name: fork_branch, message: 'Committing something',
+ actions: [{ action: :create, file_path: 'encoding/CHANGELOG', content: 'New file' }]
+ )
+
+ expect_to_cancel_exclusive_lease
+ expect(details).not_to have_conflicts
+
+ expect do
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq("9:merging commits: merge: there are conflicting files.")
+ end.not_to change { details.counts }
+
+ expect(details).to have_conflicts
+ end
+ end
+
+ context 'and does not have conflicts with the upstream' do
+ let(:fork_branch) { 'fork-with-new-commits' }
+
+ it 'updates the fork using merge' do
+ project.repository.commit_files(
+ user,
+ branch_name: fork_branch, message: 'Committing completely new changelog',
+ actions: [{ action: :create, file_path: 'encoding/COMPLETELY-NEW-CHANGELOG', content: 'New file' }]
+ )
+
+ commit_message = "Merge branch #{source_project.path}:#{source_project.default_branch} into #{fork_branch}"
+ expect(project.repository).to receive(:merge_to_branch).with(
+ user,
+ source_sha: source_project.commit.sha,
+ target_branch: fork_branch,
+ target_sha: project.commit(fork_branch).sha,
+ message: commit_message
+ ).and_call_original
+ expect_to_cancel_exclusive_lease
+
+ expect do
+ expect(service.execute).to be_success
+ end.to change { details.counts }.from({ ahead: 1, behind: 2 }).to({ ahead: 2, behind: 0 })
+
+ commits = project.repository.commits_between(source_project.commit.sha, project.commit(fork_branch).sha)
+ expect(commits.map(&:message)).to eq([
+ "Committing completely new changelog",
+ commit_message
+ ])
+ end
+ end
+ end
+
+ context 'when a merge cannot happen due to another ongoing merge' do
+ it 'does not merge' do
+ expect(service).to receive(:perform_merge).and_return(nil)
+
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq(described_class::ONGOING_MERGE_ERROR)
+ end
+ end
+
+ context 'when upstream branch contains lfs reference' do
+ let(:source_project) { create(:project, :repository, :public) }
+ let(:project) { fork_project(source_project, user, { repository: true }) }
+ let(:fork_branch) { 'fork-fetches-lfs-pointers' }
+
+ before do
+ source_project.change_head('lfs')
+
+ allow(source_project).to receive(:lfs_enabled?).and_return(true)
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+
+ create_file_in_repo(source_project, 'lfs', 'lfs', 'one.lfs', 'One')
+ create_file_in_repo(source_project, 'lfs', 'lfs', 'two.lfs', 'Two')
+ end
+
+ it 'links fetched lfs objects to the fork project', :aggregate_failures do
+ expect_to_cancel_exclusive_lease
+
+ expect do
+ expect(service.execute).to be_success
+ end.to change { project.reload.lfs_objects.size }.from(0).to(2)
+ .and change { details.counts }.from({ ahead: 0, behind: 3 }).to({ ahead: 0, behind: 0 })
+
+ expect(project.lfs_objects).to match_array(source_project.lfs_objects)
+ end
+
+ context 'and there are too many of them for a single sync' do
+ let(:fork_branch) { 'fork-too-many-lfs-pointers' }
+
+ it 'updates the fork successfully' do
+ expect_to_cancel_exclusive_lease
+ stub_const('Projects::LfsPointers::LfsLinkService::MAX_OIDS', 1)
+
+ expect do
+ result = service.execute
+
+ expect(result).to be_error
+ expect(result.message).to eq('Too many LFS object ids to link, please push them manually')
+ end.not_to change { details.counts }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/forks_count_service_spec.rb b/spec/services/projects/forks_count_service_spec.rb
index 31662f78973..403d8656b7c 100644
--- a/spec/services/projects/forks_count_service_spec.rb
+++ b/spec/services/projects/forks_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ForksCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Projects::ForksCountService, :use_clean_rails_memory_store_caching, feature_category: :source_code_management do
let(:project) { build(:project) }
subject { described_class.new(project) }
diff --git a/spec/services/projects/git_deduplication_service_spec.rb b/spec/services/projects/git_deduplication_service_spec.rb
index e6eff936de7..314c9d160dd 100644
--- a/spec/services/projects/git_deduplication_service_spec.rb
+++ b/spec/services/projects/git_deduplication_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GitDeduplicationService do
+RSpec.describe Projects::GitDeduplicationService, feature_category: :source_code_management do
include ExclusiveLeaseHelpers
let(:pool) { create(:pool_repository, :ready) }
diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb
index d32e720a49f..b1468a40212 100644
--- a/spec/services/projects/gitlab_projects_import_service_spec.rb
+++ b/spec/services/projects/gitlab_projects_import_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GitlabProjectsImportService do
+RSpec.describe Projects::GitlabProjectsImportService, feature_category: :importers do
let_it_be(:namespace) { create(:namespace) }
let(:path) { 'test-path' }
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index eae898b4f68..b62fd0ecb68 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GroupLinks::CreateService, '#execute' do
+RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category: :subgroups do
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create(:project, namespace: create(:namespace, :with_namespace_settings)) }
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 89865d6bc3b..e1f915e18bd 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GroupLinks::DestroyService, '#execute' do
+RSpec.describe Projects::GroupLinks::DestroyService, '#execute', feature_category: :subgroups do
let_it_be(:user) { create :user }
let_it_be(:project) { create(:project, :private) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb
index 1acbb770763..b3336cb91fd 100644
--- a/spec/services/projects/group_links/update_service_spec.rb
+++ b/spec/services/projects/group_links/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GroupLinks::UpdateService, '#execute' do
+RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category: :subgroups do
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:project) { create :project }
diff --git a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
index 86e3fb3820c..01036fc2d9c 100644
--- a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
+++ b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::BaseAttachmentService do
+RSpec.describe Projects::HashedStorage::BaseAttachmentService, feature_category: :projects do
let(:project) { create(:project, :repository, storage_version: 0, skip_disk_validation: true) }
subject(:service) { described_class.new(project: project, old_disk_path: project.full_path, logger: nil) }
diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
index c8f24c6ce00..39263506bca 100644
--- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::MigrateAttachmentsService do
+RSpec.describe Projects::HashedStorage::MigrateAttachmentsService, feature_category: :projects do
subject(:service) { described_class.new(project: project, old_disk_path: project.full_path, logger: nil) }
let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) }
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index eb8d94ebfa5..bcc914e72b5 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
+RSpec.describe Projects::HashedStorage::MigrateRepositoryService, feature_category: :projects do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo, :design_repo) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
diff --git a/spec/services/projects/hashed_storage/migration_service_spec.rb b/spec/services/projects/hashed_storage/migration_service_spec.rb
index ef96c17dd85..14bfa645be2 100644
--- a/spec/services/projects/hashed_storage/migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migration_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::MigrationService do
+RSpec.describe Projects::HashedStorage::MigrationService, feature_category: :projects do
let(:project) { create(:project, :empty_repo, :wiki_repo, :legacy_storage) }
let(:logger) { double }
let!(:project_attachment) { build(:file_uploader, project: project) }
diff --git a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
index d4cb46c82ad..95491d63df2 100644
--- a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::RollbackAttachmentsService do
+RSpec.describe Projects::HashedStorage::RollbackAttachmentsService, feature_category: :projects do
subject(:service) { described_class.new(project: project, old_disk_path: project.disk_path, logger: nil) }
let(:project) { create(:project, :repository, skip_disk_validation: true) }
diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
index 385c03e6308..19f1856e39a 100644
--- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis_shared_state do
+RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis_shared_state, feature_category: :projects do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :repository, :wiki_repo, :design_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
diff --git a/spec/services/projects/hashed_storage/rollback_service_spec.rb b/spec/services/projects/hashed_storage/rollback_service_spec.rb
index 0bd63f2da2a..6d047f856ec 100644
--- a/spec/services/projects/hashed_storage/rollback_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::HashedStorage::RollbackService do
+RSpec.describe Projects::HashedStorage::RollbackService, feature_category: :projects do
let(:project) { create(:project, :empty_repo, :wiki_repo) }
let(:logger) { double }
let!(:project_attachment) { build(:file_uploader, project: project) }
diff --git a/spec/services/projects/import_error_filter_spec.rb b/spec/services/projects/import_error_filter_spec.rb
index fd31cd52cc4..be07208c7f2 100644
--- a/spec/services/projects/import_error_filter_spec.rb
+++ b/spec/services/projects/import_error_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportErrorFilter do
+RSpec.describe Projects::ImportErrorFilter, feature_category: :importers do
it 'filters any full paths' do
message = 'Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file'
diff --git a/spec/services/projects/import_export/relation_export_service_spec.rb b/spec/services/projects/import_export/relation_export_service_spec.rb
index 94f5653ee7d..4b44a37b299 100644
--- a/spec/services/projects/import_export/relation_export_service_spec.rb
+++ b/spec/services/projects/import_export/relation_export_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::RelationExportService do
+RSpec.describe Projects::ImportExport::RelationExportService, feature_category: :importers do
using RSpec::Parameterized::TableSyntax
subject(:service) { described_class.new(relation_export, 'jid') }
@@ -49,6 +49,7 @@ RSpec.describe Projects::ImportExport::RelationExportService do
expect(logger).to receive(:error).with(
export_error: '',
message: 'Project relation export failed',
+ relation: relation_export.relation,
project_export_job_id: project_export_job.id,
project_id: project_export_job.project.id,
project_name: project_export_job.project.name
@@ -78,6 +79,7 @@ RSpec.describe Projects::ImportExport::RelationExportService do
expect(logger).to receive(:error).with(
export_error: 'Error!',
message: 'Project relation export failed',
+ relation: relation_export.relation,
project_export_job_id: project_export_job.id,
project_id: project_export_job.project.id,
project_name: project_export_job.project.name
diff --git a/spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb b/spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb
index 4c51c8a4ac8..4ad6fd0edff 100644
--- a/spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb
+++ b/spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::InProductMarketingCampaignEmailsService do
+RSpec.describe Projects::InProductMarketingCampaignEmailsService, feature_category: :experimentation_adoption do
describe '#execute' do
let(:user) { create(:user, email_opted_in: true) }
let(:project) { create(:project) }
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index 80b3c4d0403..0aaaae19f5a 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
+RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService, feature_category: :source_code_management do
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
let(:lfs_endpoint) { "#{import_url}/info/lfs/objects/batch" }
let!(:project) { create(:project, import_url: import_url) }
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index c815ad38843..00c156ba538 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::LfsPointers::LfsDownloadService do
+RSpec.describe Projects::LfsPointers::LfsDownloadService, feature_category: :source_code_management do
include StubRequests
let_it_be(:project) { create(:project) }
diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
index 32b86ade81e..f1e4db55962 100644
--- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::LfsPointers::LfsImportService do
+RSpec.describe Projects::LfsPointers::LfsImportService, feature_category: :source_code_management do
let(:project) { create(:project) }
let(:user) { project.creator }
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index 0e7d16f18e8..e8a08d95bba 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::LfsPointers::LfsLinkService do
- let!(:project) { create(:project, lfs_enabled: true) }
- let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+RSpec.describe Projects::LfsPointers::LfsLinkService, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, lfs_enabled: true) }
+ let_it_be(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+
let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } }
let(:all_oids) { LfsObject.pluck(:oid, :size).to_h.merge(new_oids) }
let(:new_lfs_object) { create(:lfs_object) }
@@ -17,12 +18,26 @@ RSpec.describe Projects::LfsPointers::LfsLinkService do
describe '#execute' do
it 'raises an error when trying to link too many objects at once' do
+ stub_const("#{described_class}::MAX_OIDS", 5)
+
oids = Array.new(described_class::MAX_OIDS) { |i| "oid-#{i}" }
oids << 'the straw'
expect { subject.execute(oids) }.to raise_error(described_class::TooManyOidsError)
end
+ it 'executes a block after validation and before execution' do
+ block = instance_double(Proc)
+
+ expect(subject).to receive(:validate!).ordered
+ expect(block).to receive(:call).ordered
+ expect(subject).to receive(:link_existing_lfs_objects).ordered
+
+ subject.execute([]) do
+ block.call
+ end
+ end
+
it 'links existing lfs objects to the project' do
expect(project.lfs_objects.count).to eq 2
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
index 59eb1ed7a29..f5dcae05959 100644
--- a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
+RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService, feature_category: :source_code_management do
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch" }
let(:group) { create(:group, lfs_enabled: true) }
diff --git a/spec/services/projects/move_access_service_spec.rb b/spec/services/projects/move_access_service_spec.rb
index 45e10c3ca84..b9244002f6c 100644
--- a/spec/services/projects/move_access_service_spec.rb
+++ b/spec/services/projects/move_access_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveAccessService do
+RSpec.describe Projects::MoveAccessService, feature_category: :projects do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_with_access) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_deploy_keys_projects_service_spec.rb b/spec/services/projects/move_deploy_keys_projects_service_spec.rb
index 59674a3a4ef..b40eb4a18d1 100644
--- a/spec/services/projects/move_deploy_keys_projects_service_spec.rb
+++ b/spec/services/projects/move_deploy_keys_projects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveDeployKeysProjectsService do
+RSpec.describe Projects::MoveDeployKeysProjectsService, feature_category: :continuous_delivery do
let!(:user) { create(:user) }
let!(:project_with_deploy_keys) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_forks_service_spec.rb b/spec/services/projects/move_forks_service_spec.rb
index 7d3637b7758..093562207dd 100644
--- a/spec/services/projects/move_forks_service_spec.rb
+++ b/spec/services/projects/move_forks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveForksService do
+RSpec.describe Projects::MoveForksService, feature_category: :source_code_management do
include ProjectForksHelper
let!(:user) { create(:user) }
diff --git a/spec/services/projects/move_lfs_objects_projects_service_spec.rb b/spec/services/projects/move_lfs_objects_projects_service_spec.rb
index e3df5fed9cf..f3cc4014b1c 100644
--- a/spec/services/projects/move_lfs_objects_projects_service_spec.rb
+++ b/spec/services/projects/move_lfs_objects_projects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveLfsObjectsProjectsService do
+RSpec.describe Projects::MoveLfsObjectsProjectsService, feature_category: :source_code_management do
let!(:user) { create(:user) }
let!(:project_with_lfs_objects) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_notification_settings_service_spec.rb b/spec/services/projects/move_notification_settings_service_spec.rb
index e381ae7590f..5ef6e8a0647 100644
--- a/spec/services/projects/move_notification_settings_service_spec.rb
+++ b/spec/services/projects/move_notification_settings_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveNotificationSettingsService do
+RSpec.describe Projects::MoveNotificationSettingsService, feature_category: :projects do
let(:user) { create(:user) }
let(:project_with_notifications) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_project_authorizations_service_spec.rb b/spec/services/projects/move_project_authorizations_service_spec.rb
index d47b13ca939..6cd0b056325 100644
--- a/spec/services/projects/move_project_authorizations_service_spec.rb
+++ b/spec/services/projects/move_project_authorizations_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveProjectAuthorizationsService do
+RSpec.describe Projects::MoveProjectAuthorizationsService, feature_category: :projects do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_project_group_links_service_spec.rb b/spec/services/projects/move_project_group_links_service_spec.rb
index 1fca96a0367..cfd4b51b001 100644
--- a/spec/services/projects/move_project_group_links_service_spec.rb
+++ b/spec/services/projects/move_project_group_links_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveProjectGroupLinksService do
+RSpec.describe Projects::MoveProjectGroupLinksService, feature_category: :projects do
let!(:user) { create(:user) }
let(:project_with_groups) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_project_members_service_spec.rb b/spec/services/projects/move_project_members_service_spec.rb
index 8fbd0ba3270..364fb7faaf2 100644
--- a/spec/services/projects/move_project_members_service_spec.rb
+++ b/spec/services/projects/move_project_members_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveProjectMembersService do
+RSpec.describe Projects::MoveProjectMembersService, feature_category: :projects do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/move_users_star_projects_service_spec.rb b/spec/services/projects/move_users_star_projects_service_spec.rb
index b580d3d8772..b99e51d954b 100644
--- a/spec/services/projects/move_users_star_projects_service_spec.rb
+++ b/spec/services/projects/move_users_star_projects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MoveUsersStarProjectsService do
+RSpec.describe Projects::MoveUsersStarProjectsService, feature_category: :projects do
let!(:user) { create(:user) }
let!(:project_with_stars) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index c739fea5ecf..89405f06f5d 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::OpenIssuesCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Projects::OpenIssuesCountService, :use_clean_rails_memory_store_caching, feature_category: :team_planning do
let(:project) { create(:project) }
subject { described_class.new(project) }
diff --git a/spec/services/projects/open_merge_requests_count_service_spec.rb b/spec/services/projects/open_merge_requests_count_service_spec.rb
index 6caef181e77..74eead39ec4 100644
--- a/spec/services/projects/open_merge_requests_count_service_spec.rb
+++ b/spec/services/projects/open_merge_requests_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::OpenMergeRequestsCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Projects::OpenMergeRequestsCountService, :use_clean_rails_memory_store_caching, feature_category: :code_review_workflow do
let_it_be(:project) { create(:project) }
subject { described_class.new(project) }
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 95f2176dbc0..7babaf4d0d8 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Operations::UpdateService do
+RSpec.describe Projects::Operations::UpdateService, feature_category: :projects do
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/projects/overwrite_project_service_spec.rb b/spec/services/projects/overwrite_project_service_spec.rb
index 7038910508f..b4faf45a1cb 100644
--- a/spec/services/projects/overwrite_project_service_spec.rb
+++ b/spec/services/projects/overwrite_project_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::OverwriteProjectService do
+RSpec.describe Projects::OverwriteProjectService, feature_category: :projects do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index fc745cd669f..bd297343879 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ParticipantsService do
+RSpec.describe Projects::ParticipantsService, feature_category: :projects do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 43d23023d83..d747cc4b424 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Prometheus::Alerts::NotifyService do
+RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :metrics do
include PrometheusHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/services/projects/prometheus/metrics/destroy_service_spec.rb b/spec/services/projects/prometheus/metrics/destroy_service_spec.rb
index b4af81f2c87..4c2a959a149 100644
--- a/spec/services/projects/prometheus/metrics/destroy_service_spec.rb
+++ b/spec/services/projects/prometheus/metrics/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Prometheus::Metrics::DestroyService do
+RSpec.describe Projects::Prometheus::Metrics::DestroyService, feature_category: :metrics do
let(:metric) { create(:prometheus_metric) }
subject { described_class.new(metric) }
diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb
index 9f9e89ff8f8..21743e2a656 100644
--- a/spec/services/projects/protect_default_branch_service_spec.rb
+++ b/spec/services/projects/protect_default_branch_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ProtectDefaultBranchService do
+RSpec.describe Projects::ProtectDefaultBranchService, feature_category: :source_code_management do
let(:service) { described_class.new(project) }
let(:project) { create(:project) }
diff --git a/spec/services/projects/readme_renderer_service_spec.rb b/spec/services/projects/readme_renderer_service_spec.rb
index 14cdcf67640..58b967b7e9c 100644
--- a/spec/services/projects/readme_renderer_service_spec.rb
+++ b/spec/services/projects/readme_renderer_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ReadmeRendererService, '#execute' do
+RSpec.describe Projects::ReadmeRendererService, '#execute', feature_category: :projects do
using RSpec::Parameterized::TableSyntax
subject(:service) { described_class.new(project, nil, opts) }
diff --git a/spec/services/projects/record_target_platforms_service_spec.rb b/spec/services/projects/record_target_platforms_service_spec.rb
index 22ff325a62e..dd4e9fe19e0 100644
--- a/spec/services/projects/record_target_platforms_service_spec.rb
+++ b/spec/services/projects/record_target_platforms_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RecordTargetPlatformsService, '#execute' do
+RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_category: :projects do
let_it_be(:project) { create(:project) }
let(:detector_service) { Projects::AppleTargetPlatformDetectorService }
diff --git a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb
index 62330441d2f..591cd1cba8d 100644
--- a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb
+++ b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitlab_redis_shared_state do
+RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitlab_redis_shared_state, feature_category: :build_artifacts do
let(:service) { described_class.new }
describe '#execute' do
diff --git a/spec/services/projects/repository_languages_service_spec.rb b/spec/services/projects/repository_languages_service_spec.rb
index 50d5fba6b84..a02844309b2 100644
--- a/spec/services/projects/repository_languages_service_spec.rb
+++ b/spec/services/projects/repository_languages_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RepositoryLanguagesService do
+RSpec.describe Projects::RepositoryLanguagesService, feature_category: :source_code_management do
let(:service) { described_class.new(project, project.first_owner) }
context 'when detected_repository_languages flag is set' do
diff --git a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
index 76830396104..3d89f6efa6f 100644
--- a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
+++ b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ScheduleBulkRepositoryShardMovesService do
+RSpec.describe Projects::ScheduleBulkRepositoryShardMovesService, feature_category: :source_code_management do
it_behaves_like 'moves repository shard in bulk' do
let_it_be_with_reload(:container) { create(:project, :repository) }
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 32818535146..755ee795ebe 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::TransferService do
+RSpec.describe Projects::TransferService, feature_category: :projects do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') }
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index d939a79b7e9..f9fb1f65550 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_caching do
+RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_caching, feature_category: :source_code_management do
include ProjectForksHelper
subject { described_class.new(forked_project, user) }
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index d908a169898..8157dce4ce8 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Projects::UpdatePagesService do
+RSpec.describe Projects::UpdatePagesService, feature_category: :pages do
let_it_be(:project, refind: true) { create(:project, :repository) }
let_it_be(:old_pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index 547641867bc..b65f7a50e4c 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::UpdateRemoteMirrorService do
+RSpec.describe Projects::UpdateRemoteMirrorService, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository, lfs_enabled: true) }
let_it_be(:remote_project) { create(:forked_project_with_submodules) }
let_it_be(:remote_mirror) { create(:remote_mirror, project: project, enabled: true) }
diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb
index ee8f7fb2ef2..af920d51776 100644
--- a/spec/services/projects/update_repository_storage_service_spec.rb
+++ b/spec/services/projects/update_repository_storage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::UpdateRepositoryStorageService do
+RSpec.describe Projects::UpdateRepositoryStorageService, feature_category: :source_code_management do
include Gitlab::ShellAdapter
subject { described_class.new(repository_storage_move) }
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 3cda6bc2627..bdb2e6e09e5 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::UpdateService do
+RSpec.describe Projects::UpdateService, feature_category: :projects do
include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
@@ -227,48 +227,16 @@ RSpec.describe Projects::UpdateService do
let(:user) { project.first_owner }
let(:forked_project) { fork_project(project) }
- context 'and unlink forks feature flag is off' do
- before do
- stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
- end
-
- it 'updates forks visibility level when parent set to more restrictive' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
-
- expect(project).to be_internal
- expect(forked_project).to be_internal
-
- expect(update_project(project, user, opts)).to eq({ status: :success })
-
- expect(project).to be_private
- expect(forked_project.reload).to be_private
- end
-
- it 'does not update forks visibility level when parent set to less restrictive' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+ it 'does not change visibility of forks' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
- expect(project).to be_internal
- expect(forked_project).to be_internal
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- expect(update_project(project, user, opts)).to eq({ status: :success })
+ expect(update_project(project, user, opts)).to eq({ status: :success })
- expect(project).to be_public
- expect(forked_project.reload).to be_internal
- end
- end
-
- context 'and unlink forks feature flag is on' do
- it 'does not change visibility of forks' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
-
- expect(project).to be_internal
- expect(forked_project).to be_internal
-
- expect(update_project(project, user, opts)).to eq({ status: :success })
-
- expect(project).to be_private
- expect(forked_project.reload).to be_internal
- end
+ expect(project).to be_private
+ expect(forked_project.reload).to be_internal
end
end
@@ -794,6 +762,112 @@ RSpec.describe Projects::UpdateService do
expect(project.topic_list).to eq(%w[tag_list])
end
end
+
+ describe 'when updating pages unique domain', feature_category: :pages do
+ let(:group) { create(:group, path: 'group') }
+ let(:project) { create(:project, path: 'project', group: group) }
+
+ context 'with pages_unique_domain feature flag disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
+
+ it 'does not change pages unique domain' do
+ expect(project)
+ .to receive(:update)
+ .with({ project_setting_attributes: { has_confluence: true } })
+ .and_call_original
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ has_confluence: true,
+ pages_unique_domain_enabled: true
+ })
+ end.not_to change { project.project_setting.pages_unique_domain_enabled }
+ end
+
+ it 'does not remove other attributes' do
+ expect(project)
+ .to receive(:update)
+ .with({ name: 'True' })
+ .and_call_original
+
+ update_project(project, user, name: 'True')
+ end
+ end
+
+ context 'with pages_unique_domain feature flag enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ it 'updates project pages unique domain' do
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+ end.to change { project.project_setting.pages_unique_domain_enabled }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq true
+ expect(project.project_setting.pages_unique_domain).to match %r{project-group-\w+}
+ end
+
+ it 'does not changes unique domain when it already exists' do
+ project.project_setting.update!(
+ pages_unique_domain_enabled: false,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+ end.to change { project.project_setting.pages_unique_domain_enabled }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq true
+ expect(project.project_setting.pages_unique_domain).to eq 'unique-domain'
+ end
+
+ it 'does not changes unique domain when it disabling unique domain' do
+ project.project_setting.update!(
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: false
+ })
+ end.not_to change { project.project_setting.pages_unique_domain }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq false
+ expect(project.project_setting.pages_unique_domain).to eq 'unique-domain'
+ end
+
+ context 'when there is another project with the unique domain' do
+ it 'fails pages unique domain already exists' do
+ create(
+ :project_setting,
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ allow(Gitlab::Pages::RandomDomain)
+ .to receive(:generate)
+ .and_return('unique-domain')
+
+ result = update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+
+ expect(result).to eq(
+ status: :error,
+ message: 'Project setting pages unique domain has already been taken'
+ )
+ end
+ end
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index 1cc69e7e2fe..313e1c29cd2 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::UpdateStatisticsService do
+RSpec.describe Projects::UpdateStatisticsService, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
let(:service) { described_class.new(project, nil, statistics: statistics) }
diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb
index b78683cace7..f71662f62ad 100644
--- a/spec/services/prometheus/proxy_service_spec.rb
+++ b/spec/services/prometheus/proxy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Prometheus::ProxyService do
+RSpec.describe Prometheus::ProxyService, feature_category: :metrics do
include ReactiveCachingHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
index d8c1fdffb98..fbee4b9c7d7 100644
--- a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
+++ b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Prometheus::ProxyVariableSubstitutionService do
+RSpec.describe Prometheus::ProxyVariableSubstitutionService, feature_category: :metrics do
describe '#execute' do
let_it_be(:environment) { create(:environment) }
diff --git a/spec/services/protected_branches/api_service_spec.rb b/spec/services/protected_branches/api_service_spec.rb
index c98e253267b..f7f5f451a49 100644
--- a/spec/services/protected_branches/api_service_spec.rb
+++ b/spec/services/protected_branches/api_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranches::ApiService do
+RSpec.describe ProtectedBranches::ApiService, feature_category: :compliance_management do
shared_examples 'execute with entity' do
it 'creates a protected branch with prefilled defaults' do
expect(::ProtectedBranches::CreateService).to receive(:new).with(
diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb
index ea434922661..3aa3b56640b 100644
--- a/spec/services/protected_branches/cache_service_spec.rb
+++ b/spec/services/protected_branches/cache_service_spec.rb
@@ -3,7 +3,7 @@
#
require 'spec_helper'
-RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do
+RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache, feature_category: :compliance_management do
shared_examples 'execute with entity' do
subject(:service) { described_class.new(entity, user) }
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
index 421d4aae5bb..e02b4475c02 100644
--- a/spec/services/protected_branches/destroy_service_spec.rb
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranches::DestroyService do
+RSpec.describe ProtectedBranches::DestroyService, feature_category: :compliance_management do
shared_examples 'execute with entity' do
subject(:service) { described_class.new(entity, user) }
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index c70cc032a6a..8b11604aa15 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranches::UpdateService do
+RSpec.describe ProtectedBranches::UpdateService, feature_category: :compliance_management do
shared_examples 'execute with entity' do
let(:params) { { name: new_name } }
diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb
index a0b99b595e3..78b14aae029 100644
--- a/spec/services/protected_tags/create_service_spec.rb
+++ b/spec/services/protected_tags/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedTags::CreateService do
+RSpec.describe ProtectedTags::CreateService, feature_category: :compliance_management do
let(:project) { create(:project) }
let(:user) { project.first_owner }
let(:params) do
diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb
index 658a4f5557e..fcb30d39520 100644
--- a/spec/services/protected_tags/destroy_service_spec.rb
+++ b/spec/services/protected_tags/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedTags::DestroyService do
+RSpec.describe ProtectedTags::DestroyService, feature_category: :compliance_management do
let(:protected_tag) { create(:protected_tag) }
let(:project) { protected_tag.project }
let(:user) { project.first_owner }
diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb
index 4b6e726bb6e..2fb6cf84719 100644
--- a/spec/services/protected_tags/update_service_spec.rb
+++ b/spec/services/protected_tags/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedTags::UpdateService do
+RSpec.describe ProtectedTags::UpdateService, feature_category: :compliance_management do
let(:protected_tag) { create(:protected_tag) }
let(:project) { protected_tag.project }
let(:user) { project.first_owner }
diff --git a/spec/services/push_event_payload_service_spec.rb b/spec/services/push_event_payload_service_spec.rb
index de2bec21a3c..50da5ca9b24 100644
--- a/spec/services/push_event_payload_service_spec.rb
+++ b/spec/services/push_event_payload_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PushEventPayloadService do
+RSpec.describe PushEventPayloadService, feature_category: :source_code_management do
let(:event) { create(:push_event) }
describe '#execute' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 257e7eb972b..b8abfd1e6ba 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -746,10 +746,10 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'assigns to users with escaped underscores' do
user = create(:user)
base = user.username
- user.update!(username: "#{base}_")
+ user.update!(username: "#{base}_new")
issuable.project.add_developer(user)
- cmd = "/assign @#{base}\\_"
+ cmd = "/assign @#{base}\\_new"
_, updates, _ = service.execute(cmd, issuable)
diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb
index 1b0a5d4ae73..5f4e92cf955 100644
--- a/spec/services/quick_actions/target_service_spec.rb
+++ b/spec/services/quick_actions/target_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe QuickActions::TargetService do
+RSpec.describe QuickActions::TargetService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
diff --git a/spec/services/releases/create_evidence_service_spec.rb b/spec/services/releases/create_evidence_service_spec.rb
index 0ac15a7291d..75d0a2b9c0e 100644
--- a/spec/services/releases/create_evidence_service_spec.rb
+++ b/spec/services/releases/create_evidence_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::CreateEvidenceService do
+RSpec.describe Releases::CreateEvidenceService, feature_category: :release_orchestration do
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
diff --git a/spec/services/releases/destroy_service_spec.rb b/spec/services/releases/destroy_service_spec.rb
index 46550ac5bef..953490ac379 100644
--- a/spec/services/releases/destroy_service_spec.rb
+++ b/spec/services/releases/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::DestroyService do
+RSpec.describe Releases::DestroyService, feature_category: :release_orchestration do
let(:project) { create(:project, :repository) }
let(:mainatiner) { create(:user) }
let(:repoter) { create(:user) }
diff --git a/spec/services/releases/links/create_service_spec.rb b/spec/services/releases/links/create_service_spec.rb
new file mode 100644
index 00000000000..9928d2162d7
--- /dev/null
+++ b/spec/services/releases/links/create_service_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::Links::CreateService, feature_category: :release_orchestration do
+ let(:service) { described_class.new(release, user, params) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }
+
+ let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type } }
+ let(:name) { 'link' }
+ let(:url) { 'https://example.com' }
+ let(:direct_asset_path) { '/path' }
+ let(:link_type) { 'other' }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute }
+
+ let(:link) { subject.payload[:link] }
+
+ it 'successfully creates a release link' do
+ expect { execute }.to change { Releases::Link.count }.by(1)
+
+ expect(link).to have_attributes(
+ name: name,
+ url: url,
+ filepath: direct_asset_path,
+ link_type: link_type
+ )
+ end
+
+ context 'when user does not have access to create release link' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns an error' do
+ expect { execute }.not_to change { Releases::Link.count }
+
+ is_expected.to be_error
+ expect(execute.message).to include('Access Denied')
+ expect(execute.reason).to eq(:forbidden)
+ end
+ end
+
+ context 'when url is invalid' do
+ let(:url) { 'not_a_url' }
+
+ it 'returns an error' do
+ expect { execute }.not_to change { Releases::Link.count }
+
+ is_expected.to be_error
+ expect(execute.message[0]).to include('Url is blocked')
+ expect(execute.reason).to eq(:bad_request)
+ end
+ end
+
+ context 'when both direct_asset_path and filepath are provided' do
+ let(:params) { super().merge(filepath: '/filepath') }
+
+ it 'prefers direct_asset_path' do
+ is_expected.to be_success
+
+ expect(link.filepath).to eq(direct_asset_path)
+ end
+ end
+
+ context 'when only filepath is set' do
+ let(:params) { super().merge(filepath: '/filepath') }
+ let(:direct_asset_path) { nil }
+
+ it 'uses filepath' do
+ is_expected.to be_success
+
+ expect(link.filepath).to eq('/filepath')
+ end
+ end
+ end
+end
diff --git a/spec/services/releases/links/destroy_service_spec.rb b/spec/services/releases/links/destroy_service_spec.rb
new file mode 100644
index 00000000000..a248932eada
--- /dev/null
+++ b/spec/services/releases/links/destroy_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::Links::DestroyService, feature_category: :release_orchestration do
+ let(:service) { described_class.new(release, user, {}) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }
+
+ let!(:release_link) do
+ create(
+ :release_link,
+ release: release,
+ name: 'awesome-app.dmg',
+ url: 'https://example.com/download/awesome-app.dmg'
+ )
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute(release_link) }
+
+ it 'successfully deletes a release link' do
+ expect { execute }.to change { release.links.count }.by(-1)
+
+ is_expected.to be_success
+ end
+
+ context 'when user does not have access to delete release link' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns an error' do
+ expect { execute }.not_to change { release.links.count }
+
+ is_expected.to be_error
+ expect(execute.message).to include('Access Denied')
+ expect(execute.reason).to eq(:forbidden)
+ end
+ end
+
+ context 'when release link does not exist' do
+ let(:release_link) { nil }
+
+ it 'returns an error' do
+ expect { execute }.not_to change { release.links.count }
+
+ is_expected.to be_error
+ expect(execute.message).to eq('Link does not exist')
+ expect(execute.reason).to eq(:not_found)
+ end
+ end
+
+ context 'when release link deletion failed' do
+ before do
+ allow(release_link).to receive(:destroy).and_return(false)
+ end
+
+ it 'returns an error' do
+ expect { execute }.not_to change { release.links.count }
+
+ is_expected.to be_error
+ expect(execute.reason).to eq(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/releases/links/update_service_spec.rb b/spec/services/releases/links/update_service_spec.rb
new file mode 100644
index 00000000000..3f48985cf60
--- /dev/null
+++ b/spec/services/releases/links/update_service_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::Links::UpdateService, feature_category: :release_orchestration do
+ let(:service) { described_class.new(release, user, params) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }
+
+ let(:release_link) do
+ create(
+ :release_link,
+ release: release,
+ name: 'awesome-app.dmg',
+ url: 'https://example.com/download/awesome-app.dmg'
+ )
+ end
+
+ let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type } }
+ let(:name) { 'link' }
+ let(:url) { 'https://example.com' }
+ let(:direct_asset_path) { '/path' }
+ let(:link_type) { 'other' }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute(release_link) }
+
+ let(:updated_link) { execute.payload[:link] }
+
+ it 'successfully updates a release link' do
+ is_expected.to be_success
+
+ expect(updated_link).to have_attributes(
+ name: name,
+ url: url,
+ filepath: direct_asset_path,
+ link_type: link_type
+ )
+ end
+
+ context 'when user does not have access to update release link' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns an error' do
+ is_expected.to be_error
+ expect(execute.message).to include('Access Denied')
+ expect(execute.reason).to eq(:forbidden)
+ end
+ end
+
+ context 'when url is invalid' do
+ let(:url) { 'not_a_url' }
+
+ it 'returns an error' do
+ is_expected.to be_error
+ expect(execute.message[0]).to include('Url is blocked')
+ expect(execute.reason).to eq(:bad_request)
+ end
+ end
+
+ context 'when both direct_asset_path and filepath are provided' do
+ let(:params) { super().merge(filepath: '/filepath') }
+
+ it 'prefers direct_asset_path' do
+ is_expected.to be_success
+
+ expect(updated_link.filepath).to eq(direct_asset_path)
+ end
+ end
+
+ context 'when only filepath is set' do
+ let(:params) { super().merge(filepath: '/filepath') }
+ let(:direct_asset_path) { nil }
+
+ it 'uses filepath' do
+ is_expected.to be_success
+
+ expect(updated_link.filepath).to eq('/filepath')
+ end
+ end
+ end
+end
diff --git a/spec/services/repositories/changelog_service_spec.rb b/spec/services/repositories/changelog_service_spec.rb
index 42b586637ad..1b5300672e3 100644
--- a/spec/services/repositories/changelog_service_spec.rb
+++ b/spec/services/repositories/changelog_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Repositories::ChangelogService do
+RSpec.describe Repositories::ChangelogService, feature_category: :source_code_management do
describe '#execute' do
let!(:project) { create(:project, :empty_repo) }
let!(:creator) { project.creator }
diff --git a/spec/services/repositories/destroy_service_spec.rb b/spec/services/repositories/destroy_service_spec.rb
index 565a18d501a..b3bad4fd84d 100644
--- a/spec/services/repositories/destroy_service_spec.rb
+++ b/spec/services/repositories/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Repositories::DestroyService do
+RSpec.describe Repositories::DestroyService, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace) }
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
index 8db1a6858fa..1ce68080c73 100644
--- a/spec/services/repository_archive_clean_up_service_spec.rb
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryArchiveCleanUpService do
+RSpec.describe RepositoryArchiveCleanUpService, feature_category: :source_code_management do
subject(:service) { described_class.new }
describe '#execute (new archive locations)' do
diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb
index 165b38ee338..6ae516a5f07 100644
--- a/spec/services/reset_project_cache_service_spec.rb
+++ b/spec/services/reset_project_cache_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResetProjectCacheService do
+RSpec.describe ResetProjectCacheService, feature_category: :projects do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index a8c8d41ca09..464fc18f1f0 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceAccessTokens::CreateService do
+RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_access do
subject { described_class.new(user, resource, params).execute }
let_it_be(:user) { create(:user) }
@@ -99,12 +99,20 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
- context 'bot email' do
- it 'check email domain' do
- response = subject
- access_token = response.payload[:access_token]
+ context 'bot username and email' do
+ include_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do
+ subject do
+ response = described_class.new(user, resource, params).execute
+ response.payload[:access_token].user
+ end
+
+ let(:username_prefix) do
+ "#{resource.class.name.downcase}_#{resource.id}_bot"
+ end
- expect(access_token.user.email).to end_with("@noreply.#{Gitlab.config.gitlab.host}")
+ let(:email_domain) do
+ "noreply.#{Gitlab.config.gitlab.host}"
+ end
end
end
@@ -119,9 +127,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
- context 'when user specifies an access level' do
- let_it_be(:params) { { access_level: Gitlab::Access::DEVELOPER } }
-
+ shared_examples 'bot with access level' do
it 'adds the bot user with the specified access level in the resource' do
response = subject
access_token = response.payload[:access_token]
@@ -131,6 +137,18 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
+ context 'when user specifies an access level' do
+ let_it_be(:params) { { access_level: Gitlab::Access::DEVELOPER } }
+
+ it_behaves_like 'bot with access level'
+ end
+
+ context 'with DEVELOPER access_level, in string format' do
+ let_it_be(:params) { { access_level: Gitlab::Access::DEVELOPER.to_s } }
+
+ it_behaves_like 'bot with access level'
+ end
+
context 'when user is external' do
before do
user.update!(external: true)
@@ -219,17 +237,31 @@ RSpec.describe ResourceAccessTokens::CreateService do
let_it_be(:bot_user) { create(:user, :project_bot) }
let(:unpersisted_member) { build(:project_member, source: resource, user: bot_user) }
- let(:error_message) { 'Could not provision maintainer access to project access token' }
+ let(:error_message) { 'Could not provision maintainer access to the access token. ERROR: error message' }
before do
allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
allow(service).to receive(:create_user).and_return(bot_user)
allow(service).to receive(:create_membership).and_return(unpersisted_member)
end
+
+ allow(unpersisted_member).to receive_message_chain(:errors, :full_messages, :to_sentence)
+ .and_return('error message')
+ end
+
+ context 'with MAINTAINER access_level, in integer format' do
+ let_it_be(:params) { { access_level: Gitlab::Access::MAINTAINER } }
+
+ it_behaves_like 'token creation fails'
+ it_behaves_like 'correct error message'
end
- it_behaves_like 'token creation fails'
- it_behaves_like 'correct error message'
+ context 'with MAINTAINER access_level, in string format' do
+ let_it_be(:params) { { access_level: Gitlab::Access::MAINTAINER.to_s } }
+
+ it_behaves_like 'token creation fails'
+ it_behaves_like 'correct error message'
+ end
end
end
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index 28f173f1bc7..c00146961e3 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceAccessTokens::RevokeService do
+RSpec.describe ResourceAccessTokens::RevokeService, feature_category: :system_access do
subject { described_class.new(user, resource, access_token).execute }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index d94b49de9d7..8393ce78df8 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# feature category is shared among plan(issues, epics), monitor(incidents), create(merge request) stages
-RSpec.describe ResourceEvents::ChangeLabelsService, feature_category: :shared do
+RSpec.describe ResourceEvents::ChangeLabelsService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:author) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
index 425d5b19907..077058df1d5 100644
--- a/spec/services/resource_events/change_milestone_service_spec.rb
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::ChangeMilestoneService do
+RSpec.describe ResourceEvents::ChangeMilestoneService, feature_category: :team_planning do
let_it_be(:timebox) { create(:milestone) }
let(:created_at_time) { Time.utc(2019, 12, 30) }
diff --git a/spec/services/resource_events/change_state_service_spec.rb b/spec/services/resource_events/change_state_service_spec.rb
index b679943073c..a63b4302635 100644
--- a/spec/services/resource_events/change_state_service_spec.rb
+++ b/spec/services/resource_events/change_state_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::ChangeStateService do
+RSpec.describe ResourceEvents::ChangeStateService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb
index ebfd942066f..6eb6780d704 100644
--- a/spec/services/resource_events/merge_into_notes_service_spec.rb
+++ b/spec/services/resource_events/merge_into_notes_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::MergeIntoNotesService do
+RSpec.describe ResourceEvents::MergeIntoNotesService, feature_category: :team_planning do
def create_event(params)
event_params = { action: :add, label: label, issue: resource,
user: user }
diff --git a/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb
index 71b1d0993ee..3396abaff9e 100644
--- a/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb
+++ b/spec/services/resource_events/synthetic_label_notes_builder_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::SyntheticLabelNotesBuilderService do
+RSpec.describe ResourceEvents::SyntheticLabelNotesBuilderService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb
index f368e107c60..ca61afc7914 100644
--- a/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb
+++ b/spec/services/resource_events/synthetic_milestone_notes_builder_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::SyntheticMilestoneNotesBuilderService do
+RSpec.describe ResourceEvents::SyntheticMilestoneNotesBuilderService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, author: user) }
diff --git a/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb b/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb
index 79500f3768b..9f838660f92 100644
--- a/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb
+++ b/spec/services/resource_events/synthetic_state_notes_builder_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::SyntheticStateNotesBuilderService do
+RSpec.describe ResourceEvents::SyntheticStateNotesBuilderService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index e8716ef4d90..6250d32574f 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Search::GlobalService do
+RSpec.describe Search::GlobalService, feature_category: :global_search do
let(:user) { create(:user) }
let(:internal_user) { create(:user) }
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
index c9bfa7cb7b4..e8a4a228f8f 100644
--- a/spec/services/search/group_service_spec.rb
+++ b/spec/services/search/group_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Search::GroupService do
+RSpec.describe Search::GroupService, feature_category: :global_search do
shared_examples_for 'group search' do
context 'finding projects by name' do
let(:user) { create(:user) }
diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb
index d204f626635..d60b60d28e4 100644
--- a/spec/services/search/snippet_service_spec.rb
+++ b/spec/services/search/snippet_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Search::SnippetService do
+RSpec.describe Search::SnippetService, feature_category: :global_search do
let_it_be(:author) { create(:author) }
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb b/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb
index df76750efc8..a56fbb026c1 100644
--- a/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb
+++ b/spec/services/security/ci_configuration/container_scanning_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Security::CiConfiguration::ContainerScanningCreateService, :snowplow do
+RSpec.describe Security::CiConfiguration::ContainerScanningCreateService, :snowplow, feature_category: :container_scanning do
subject(:result) { described_class.new(project, user).execute }
let(:branch_name) { 'set-container-scanning-config-1' }
diff --git a/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb b/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb
index deb10732b37..7f1ad543f7c 100644
--- a/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_iac_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Security::CiConfiguration::SastIacCreateService, :snowplow do
+RSpec.describe Security::CiConfiguration::SastIacCreateService, :snowplow, feature_category: :static_application_security_testing do
subject(:result) { described_class.new(project, user).execute }
let(:branch_name) { 'set-sast-iac-config-1' }
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 9211beb76f8..051bbcd194b 100644
--- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Security::CiConfiguration::SastParserService do
+RSpec.describe Security::CiConfiguration::SastParserService, feature_category: :static_application_security_testing do
describe '#configuration' do
include_context 'read ci configuration for sast enabled project'
diff --git a/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb b/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb
index c1df3ebdca5..6cbeb219d11 100644
--- a/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb
+++ b/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Security::CiConfiguration::SecretDetectionCreateService, :snowplow do
+RSpec.describe Security::CiConfiguration::SecretDetectionCreateService, :snowplow, feature_category: :container_scanning do
subject(:result) { described_class.new(project, user).execute }
let(:branch_name) { 'set-secret-detection-config-1' }
diff --git a/spec/services/security/merge_reports_service_spec.rb b/spec/services/security/merge_reports_service_spec.rb
index 249f4da5f34..809d0b27c20 100644
--- a/spec/services/security/merge_reports_service_spec.rb
+++ b/spec/services/security/merge_reports_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
-RSpec.describe Security::MergeReportsService, '#execute' do
+RSpec.describe Security::MergeReportsService, '#execute', feature_category: :code_review_workflow do
let(:scanner_1) { build(:ci_reports_security_scanner, external_id: 'scanner-1', name: 'Scanner 1') }
let(:scanner_2) { build(:ci_reports_security_scanner, external_id: 'scanner-2', name: 'Scanner 2') }
let(:scanner_3) { build(:ci_reports_security_scanner, external_id: 'scanner-3', name: 'Scanner 3') }
diff --git a/spec/services/serverless/associate_domain_service_spec.rb b/spec/services/serverless/associate_domain_service_spec.rb
deleted file mode 100644
index 2f45806589e..00000000000
--- a/spec/services/serverless/associate_domain_service_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Serverless::AssociateDomainService do
- let_it_be(:sdc_pages_domain) { create(:pages_domain, :instance_serverless) }
- let_it_be(:sdc_cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) }
- let_it_be(:sdc_knative) { create(:clusters_applications_knative, cluster: sdc_cluster) }
- let_it_be(:sdc_creator) { create(:user) }
-
- let(:sdc) do
- create(:serverless_domain_cluster,
- knative: sdc_knative,
- creator: sdc_creator,
- pages_domain: sdc_pages_domain)
- end
-
- let(:knative) { sdc.knative }
- let(:creator) { sdc.creator }
- let(:pages_domain_id) { sdc.pages_domain_id }
-
- subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) }
-
- context 'when the domain is unchanged' do
- let(:creator) { create(:user) }
-
- it 'does not update creator' do
- expect { subject.execute }.not_to change { sdc.reload.creator }
- end
- end
-
- context 'when domain is changed to nil' do
- let_it_be(:creator) { create(:user) }
- let_it_be(:pages_domain_id) { nil }
-
- it 'removes the association between knative and the domain' do
- expect { subject.execute }.to change { knative.reload.pages_domain }.from(sdc.pages_domain).to(nil)
- end
-
- it 'does not attempt to update creator' do
- expect { subject.execute }.not_to raise_error
- end
- end
-
- context 'when a new domain is associated' do
- let_it_be(:creator) { create(:user) }
- let_it_be(:pages_domain_id) { create(:pages_domain, :instance_serverless).id }
-
- it 'creates an association with the domain' do
- expect { subject.execute }.to change { knative.reload.pages_domain.id }
- .from(sdc.pages_domain.id)
- .to(pages_domain_id)
- end
-
- it 'updates creator' do
- expect { subject.execute }.to change { sdc.reload.creator }.from(sdc.creator).to(creator)
- end
- end
-
- context 'when knative is not authorized to use the pages domain' do
- let_it_be(:pages_domain_id) { create(:pages_domain).id }
-
- before do
- expect(knative).to receive(:available_domains).and_return(PagesDomain.none)
- end
-
- it 'sets pages_domain_id to nil' do
- expect { subject.execute }.to change { knative.reload.pages_domain }.from(sdc.pages_domain).to(nil)
- end
- end
-
- describe 'for new knative application' do
- let_it_be(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) }
-
- context 'when knative hostname is nil' do
- let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: nil) }
-
- it 'sets hostname to a placeholder value' do
- expect { subject.execute }.to change { knative.hostname }.to('example.com')
- end
- end
-
- context 'when knative hostname exists' do
- let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: 'hostname.com') }
-
- it 'does not change hostname' do
- expect { subject.execute }.not_to change { knative.hostname }
- end
- end
- end
-end
diff --git a/spec/services/service_desk_settings/update_service_spec.rb b/spec/services/service_desk_settings/update_service_spec.rb
index 72134af1369..342fb2b6b7a 100644
--- a/spec/services/service_desk_settings/update_service_spec.rb
+++ b/spec/services/service_desk_settings/update_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ServiceDeskSettings::UpdateService do
+RSpec.describe ServiceDeskSettings::UpdateService, feature_category: :service_desk do
describe '#execute' do
let_it_be(:settings) { create(:service_desk_setting, outgoing_name: 'original name') }
let_it_be(:user) { create(:user) }
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 b02f1e84d25..2248febda5c 100644
--- a/spec/services/service_ping/submit_service_ping_service_spec.rb
+++ b/spec/services/service_ping/submit_service_ping_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ServicePing::SubmitService do
+RSpec.describe ServicePing::SubmitService, feature_category: :service_ping do
include StubRequests
include UsageDataHelpers
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index 58dd2fd4c5e..6171ca1a8a6 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -7,7 +7,7 @@ require 're2'
require_relative '../../app/services/service_response'
require_relative '../../lib/gitlab/error_tracking'
-RSpec.describe ServiceResponse do
+RSpec.describe ServiceResponse, feature_category: :shared do
describe '.success' do
it 'creates a successful response without a message' do
expect(described_class.success).to be_success
diff --git a/spec/services/snippets/bulk_destroy_service_spec.rb b/spec/services/snippets/bulk_destroy_service_spec.rb
index 4142aa349e4..208386aee48 100644
--- a/spec/services/snippets/bulk_destroy_service_spec.rb
+++ b/spec/services/snippets/bulk_destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::BulkDestroyService do
+RSpec.describe Snippets::BulkDestroyService, feature_category: :source_code_management do
let_it_be(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/services/snippets/count_service_spec.rb b/spec/services/snippets/count_service_spec.rb
index 5ce637d0bac..4ad9b07d518 100644
--- a/spec/services/snippets/count_service_spec.rb
+++ b/spec/services/snippets/count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::CountService do
+RSpec.describe Snippets::CountService, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb
index 0eb73c8edd2..725f1b165a2 100644
--- a/spec/services/snippets/create_service_spec.rb
+++ b/spec/services/snippets/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::CreateService do
+RSpec.describe Snippets::CreateService, feature_category: :source_code_management do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
diff --git a/spec/services/snippets/destroy_service_spec.rb b/spec/services/snippets/destroy_service_spec.rb
index 23765243dd6..d78b5429189 100644
--- a/spec/services/snippets/destroy_service_spec.rb
+++ b/spec/services/snippets/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::DestroyService do
+RSpec.describe Snippets::DestroyService, feature_category: :source_code_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
diff --git a/spec/services/snippets/repository_validation_service_spec.rb b/spec/services/snippets/repository_validation_service_spec.rb
index 8166ce144e1..c9cd9f21481 100644
--- a/spec/services/snippets/repository_validation_service_spec.rb
+++ b/spec/services/snippets/repository_validation_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::RepositoryValidationService do
+RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_code_management do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :empty_repo, author: user) }
diff --git a/spec/services/snippets/schedule_bulk_repository_shard_moves_service_spec.rb b/spec/services/snippets/schedule_bulk_repository_shard_moves_service_spec.rb
index 9286d73ed4a..e88969ccf2d 100644
--- a/spec/services/snippets/schedule_bulk_repository_shard_moves_service_spec.rb
+++ b/spec/services/snippets/schedule_bulk_repository_shard_moves_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::ScheduleBulkRepositoryShardMovesService do
+RSpec.describe Snippets::ScheduleBulkRepositoryShardMovesService, feature_category: :source_code_management do
it_behaves_like 'moves repository shard in bulk' do
let_it_be_with_reload(:container) { create(:snippet, :repository) }
diff --git a/spec/services/snippets/update_repository_storage_service_spec.rb b/spec/services/snippets/update_repository_storage_service_spec.rb
index 9874189f73a..c417fbfd8b1 100644
--- a/spec/services/snippets/update_repository_storage_service_spec.rb
+++ b/spec/services/snippets/update_repository_storage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::UpdateRepositoryStorageService do
+RSpec.describe Snippets::UpdateRepositoryStorageService, feature_category: :source_code_management do
subject { described_class.new(repository_storage_move) }
describe "#execute" do
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index 67cc258b4b6..99bb70a3077 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::UpdateService do
+RSpec.describe Snippets::UpdateService, feature_category: :source_code_management do
describe '#execute', :aggregate_failures do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create :user, admin: true }
diff --git a/spec/services/snippets/update_statistics_service_spec.rb b/spec/services/snippets/update_statistics_service_spec.rb
index 27ae054676a..2d1872a09c4 100644
--- a/spec/services/snippets/update_statistics_service_spec.rb
+++ b/spec/services/snippets/update_statistics_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::UpdateStatisticsService do
+RSpec.describe Snippets::UpdateStatisticsService, feature_category: :source_code_management do
describe '#execute' do
subject { described_class.new(snippet).execute }
diff --git a/spec/services/spam/akismet_mark_as_spam_service_spec.rb b/spec/services/spam/akismet_mark_as_spam_service_spec.rb
index 12666e23e47..f07fa8d262b 100644
--- a/spec/services/spam/akismet_mark_as_spam_service_spec.rb
+++ b/spec/services/spam/akismet_mark_as_spam_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Spam::AkismetMarkAsSpamService do
+RSpec.describe Spam::AkismetMarkAsSpamService, feature_category: :instance_resiliency do
let(:user_agent_detail) { build(:user_agent_detail) }
let(:spammable) { build(:issue, user_agent_detail: user_agent_detail) }
let(:fake_akismet_service) { double(:akismet_service, submit_spam: true) }
diff --git a/spec/services/spam/akismet_service_spec.rb b/spec/services/spam/akismet_service_spec.rb
index d9f62258a53..4d6a1650327 100644
--- a/spec/services/spam/akismet_service_spec.rb
+++ b/spec/services/spam/akismet_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Spam::AkismetService do
+RSpec.describe Spam::AkismetService, feature_category: :instance_resiliency do
let(:fake_akismet_client) { double(:akismet_client) }
let(:ip) { '1.2.3.4' }
let(:user_agent) { 'some user_agent' }
diff --git a/spec/services/spam/ham_service_spec.rb b/spec/services/spam/ham_service_spec.rb
index 0101a8e7704..00906bc4b3d 100644
--- a/spec/services/spam/ham_service_spec.rb
+++ b/spec/services/spam/ham_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Spam::HamService do
+RSpec.describe Spam::HamService, feature_category: :instance_resiliency do
let_it_be(:user) { create(:user) }
let!(:spam_log) { create(:spam_log, user: user, submitted_as_ham: false) }
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
index 4dfec9735ba..882b325b7b7 100644
--- a/spec/services/spam/spam_action_service_spec.rb
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Spam::SpamActionService do
+RSpec.describe Spam::SpamActionService, feature_category: :instance_resiliency do
include_context 'includes Spam constants'
let(:issue) { create(:issue, project: project, author: author) }
diff --git a/spec/services/spam/spam_params_spec.rb b/spec/services/spam/spam_params_spec.rb
index 7e74641c0fa..39c3b303529 100644
--- a/spec/services/spam/spam_params_spec.rb
+++ b/spec/services/spam/spam_params_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Spam::SpamParams do
+RSpec.describe Spam::SpamParams, feature_category: :instance_resiliency do
shared_examples 'constructs from a request' do
it 'constructs from a request' do
expected = ::Spam::SpamParams.new(
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index dde93aa6b93..42993491459 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Spam::SpamVerdictService do
+RSpec.describe Spam::SpamVerdictService, feature_category: :instance_resiliency do
include_context 'includes Spam constants'
let(:fake_ip) { '1.2.3.4' }
diff --git a/spec/services/submodules/update_service_spec.rb b/spec/services/submodules/update_service_spec.rb
index 1a53da7b9fe..aeaf8ec1c7b 100644
--- a/spec/services/submodules/update_service_spec.rb
+++ b/spec/services/submodules/update_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Submodules::UpdateService do
+RSpec.describe Submodules::UpdateService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user, :commit_email) }
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index 41ccd8523fa..6e2c623035e 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Suggestions::ApplyService do
+RSpec.describe Suggestions::ApplyService, feature_category: :code_suggestions do
include ProjectForksHelper
def build_position(**optional_args)
diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb
index a4e62431128..a8bc3cba697 100644
--- a/spec/services/suggestions/create_service_spec.rb
+++ b/spec/services/suggestions/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Suggestions::CreateService do
+RSpec.describe Suggestions::CreateService, feature_category: :code_suggestions do
let(:project_with_repo) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request, source_project: project_with_repo,
diff --git a/spec/services/suggestions/outdate_service_spec.rb b/spec/services/suggestions/outdate_service_spec.rb
index e8891f88548..7bd70866bf7 100644
--- a/spec/services/suggestions/outdate_service_spec.rb
+++ b/spec/services/suggestions/outdate_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Suggestions::OutdateService do
+RSpec.describe Suggestions::OutdateService, feature_category: :code_suggestions do
describe '#execute' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 5d60b6e0487..883a7d3a2ce 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemHooksService do
+RSpec.describe SystemHooksService, feature_category: :webhooks do
describe '#execute_hooks_for' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/system_notes/alert_management_service_spec.rb b/spec/services/system_notes/alert_management_service_spec.rb
index 039975c1bf6..4d40a6a6cfd 100644
--- a/spec/services/system_notes/alert_management_service_spec.rb
+++ b/spec/services/system_notes/alert_management_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::SystemNotes::AlertManagementService do
+RSpec.describe ::SystemNotes::AlertManagementService, feature_category: :projects do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:noteable) { create(:alert_management_alert, :with_incident, :acknowledged, project: project) }
diff --git a/spec/services/system_notes/base_service_spec.rb b/spec/services/system_notes/base_service_spec.rb
index efb165f8e4c..6ea4751b613 100644
--- a/spec/services/system_notes/base_service_spec.rb
+++ b/spec/services/system_notes/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemNotes::BaseService do
+RSpec.describe SystemNotes::BaseService, feature_category: :projects do
let(:noteable) { double }
let(:project) { double }
let(:author) { double }
diff --git a/spec/services/system_notes/commit_service_spec.rb b/spec/services/system_notes/commit_service_spec.rb
index 0399603980d..8dfb83f63fe 100644
--- a/spec/services/system_notes/commit_service_spec.rb
+++ b/spec/services/system_notes/commit_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemNotes::CommitService do
+RSpec.describe SystemNotes::CommitService, feature_category: :code_review_workflow do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:author) { create(:user) }
@@ -13,7 +13,7 @@ RSpec.describe SystemNotes::CommitService do
subject { commit_service.add_commits(new_commits, old_commits, oldrev) }
let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
- let(:new_commits) { noteable.commits }
+ let(:new_commits) { create_commits(10) }
let(:old_commits) { [] }
let(:oldrev) { nil }
@@ -43,6 +43,48 @@ RSpec.describe SystemNotes::CommitService do
expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>")
end
end
+
+ context 'with HTML content' do
+ let(:new_commits) { [double(title: '<pre>This is a test</pre>', short_id: '12345678')] }
+
+ it 'escapes HTML titles' do
+ expect(note_lines[1]).to eq("<ul><li>12345678 - &lt;pre&gt;This is a test&lt;/pre&gt;</li></ul>")
+ end
+ end
+
+ context 'with one commit exceeding the NEW_COMMIT_DISPLAY_LIMIT' do
+ let(:new_commits) { create_commits(11) }
+ let(:earlier_commit_summary_line) { note_lines[1] }
+
+ it 'includes the truncated new commits summary' do
+ expect(earlier_commit_summary_line).to start_with("<ul><li>#{new_commits[0].short_id} - 1 earlier commit")
+ end
+
+ context 'with oldrev' do
+ let(:oldrev) { '12345678abcd' }
+
+ it 'includes the truncated new commits summary with the oldrev' do
+ expect(earlier_commit_summary_line).to start_with("<ul><li>#{new_commits[0].short_id} - 1 earlier commit")
+ end
+ end
+ end
+
+ context 'with multiple commits exceeding the NEW_COMMIT_DISPLAY_LIMIT' do
+ let(:new_commits) { create_commits(13) }
+ let(:earlier_commit_summary_line) { note_lines[1] }
+
+ it 'includes the truncated new commits summary' do
+ expect(earlier_commit_summary_line).to start_with("<ul><li>#{new_commits[0].short_id}..#{new_commits[2].short_id} - 3 earlier commits")
+ end
+
+ context 'with oldrev' do
+ let(:oldrev) { '12345678abcd' }
+
+ it 'includes the truncated new commits summary with the oldrev' do
+ expect(earlier_commit_summary_line).to start_with("<ul><li>12345678...#{new_commits[2].short_id} - 3 earlier commits")
+ end
+ end
+ end
end
describe 'summary line for existing commits' do
@@ -54,6 +96,15 @@ RSpec.describe SystemNotes::CommitService do
it 'includes the existing commit' do
expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>")
end
+
+ context 'with new commits exceeding the display limit' do
+ let(:summary_line) { note_lines[1] }
+ let(:new_commits) { create_commits(13) }
+
+ it 'includes the existing commit as well as the truncated new commit summary' do
+ expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code></li><li>#{old_commits.last.short_id}...#{new_commits[2].short_id} - 3 earlier commits")
+ end
+ end
end
context 'with multiple existing commits' do
@@ -66,6 +117,15 @@ RSpec.describe SystemNotes::CommitService do
expect(summary_line)
.to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>")
end
+
+ context 'with new commits exceeding the display limit' do
+ let(:new_commits) { create_commits(13) }
+
+ it 'includes the existing commit as well as the truncated new commit summary' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code></li><li>#{old_commits.last.short_id}...#{new_commits[2].short_id} - 3 earlier commits")
+ end
+ end
end
context 'without oldrev' do
@@ -73,6 +133,15 @@ RSpec.describe SystemNotes::CommitService do
expect(summary_line)
.to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>")
end
+
+ context 'with new commits exceeding the display limit' do
+ let(:new_commits) { create_commits(13) }
+
+ it 'includes the existing commit as well as the truncated new commit summary' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{old_commits.first.short_id}..#{old_commits.last.short_id} - 26 commits from branch <code>feature</code></li><li>#{old_commits.last.short_id}...#{new_commits[2].short_id} - 3 earlier commits")
+ end
+ end
end
context 'on a fork' do
@@ -106,12 +175,9 @@ RSpec.describe SystemNotes::CommitService do
end
end
- describe '#new_commit_summary' do
- it 'escapes HTML titles' do
- commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
- escaped = '&lt;pre&gt;This is a test&lt;/pre&gt;'
-
- expect(described_class.new.new_commit_summary([commit])).to all(match(/- #{escaped}/))
+ def create_commits(count)
+ Array.new(count) do |i|
+ double(title: "Test commit #{i}", short_id: "abcd00#{i}")
end
end
end
diff --git a/spec/services/system_notes/design_management_service_spec.rb b/spec/services/system_notes/design_management_service_spec.rb
index 19e1f338eb8..92568890c6f 100644
--- a/spec/services/system_notes/design_management_service_spec.rb
+++ b/spec/services/system_notes/design_management_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemNotes::DesignManagementService do
+RSpec.describe SystemNotes::DesignManagementService, feature_category: :design_management do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/services/system_notes/incident_service_spec.rb b/spec/services/system_notes/incident_service_spec.rb
index 5de352ad8fa..0e9828c0a81 100644
--- a/spec/services/system_notes/incident_service_spec.rb
+++ b/spec/services/system_notes/incident_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::SystemNotes::IncidentService do
+RSpec.describe ::SystemNotes::IncidentService, feature_category: :incident_management do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:noteable) { create(:incident, project: project) }
diff --git a/spec/services/system_notes/incidents_service_spec.rb b/spec/services/system_notes/incidents_service_spec.rb
index 6439f9fae93..5452d51dfc0 100644
--- a/spec/services/system_notes/incidents_service_spec.rb
+++ b/spec/services/system_notes/incidents_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemNotes::IncidentsService do
+RSpec.describe SystemNotes::IncidentsService, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:author) { create(:user) }
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 3263e410d3c..08a91234174 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::SystemNotes::IssuablesService do
+RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning do
include ProjectForksHelper
let_it_be(:group) { create(:group) }
diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb
index 3e66ccef106..7ddcd799a55 100644
--- a/spec/services/system_notes/merge_requests_service_spec.rb
+++ b/spec/services/system_notes/merge_requests_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::SystemNotes::MergeRequestsService do
+RSpec.describe ::SystemNotes::MergeRequestsService, feature_category: :code_review_workflow do
include Gitlab::Routing
let_it_be(:group) { create(:group) }
diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb
index c856caa3f3e..5cc17f55012 100644
--- a/spec/services/system_notes/time_tracking_service_spec.rb
+++ b/spec/services/system_notes/time_tracking_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::SystemNotes::TimeTrackingService do
+RSpec.describe ::SystemNotes::TimeTrackingService, feature_category: :team_planning do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/services/system_notes/zoom_service_spec.rb b/spec/services/system_notes/zoom_service_spec.rb
index 986324c9664..b46b4113e12 100644
--- a/spec/services/system_notes/zoom_service_spec.rb
+++ b/spec/services/system_notes/zoom_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::SystemNotes::ZoomService do
+RSpec.describe ::SystemNotes::ZoomService, feature_category: :integrations do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:author) { create(:user) }
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index bbf6fe62959..51b8bace626 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Tags::CreateService do
+RSpec.describe Tags::CreateService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb
index 6160f337552..343a87785ad 100644
--- a/spec/services/tags/destroy_service_spec.rb
+++ b/spec/services/tags/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Tags::DestroyService do
+RSpec.describe Tags::DestroyService, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb
index f889f298213..b0444a5ba72 100644
--- a/spec/services/task_list_toggle_service_spec.rb
+++ b/spec/services/task_list_toggle_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TaskListToggleService do
+RSpec.describe TaskListToggleService, feature_category: :team_planning do
let(:markdown) do
<<-EOT.strip_heredoc
* [ ] Task 1
diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb
index cfeff36cc0d..ff4eefdfb3a 100644
--- a/spec/services/tasks_to_be_done/base_service_spec.rb
+++ b/spec/services/tasks_to_be_done/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TasksToBeDone::BaseService do
+RSpec.describe TasksToBeDone::BaseService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:assignee_one) { create(:user) }
diff --git a/spec/services/terraform/remote_state_handler_spec.rb b/spec/services/terraform/remote_state_handler_spec.rb
index 369309e4d5a..f4f7a8a0985 100644
--- a/spec/services/terraform/remote_state_handler_spec.rb
+++ b/spec/services/terraform/remote_state_handler_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Terraform::RemoteStateHandler do
+RSpec.describe Terraform::RemoteStateHandler, feature_category: :infrastructure_as_code do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
diff --git a/spec/services/terraform/states/destroy_service_spec.rb b/spec/services/terraform/states/destroy_service_spec.rb
index 5acf32cd73c..3515a758827 100644
--- a/spec/services/terraform/states/destroy_service_spec.rb
+++ b/spec/services/terraform/states/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Terraform::States::DestroyService do
+RSpec.describe Terraform::States::DestroyService, feature_category: :infrastructure_as_code do
let_it_be(:state) { create(:terraform_state, :with_version, :deletion_in_progress) }
let(:file) { instance_double(Terraform::StateUploader, relative_path: 'path') }
diff --git a/spec/services/terraform/states/trigger_destroy_service_spec.rb b/spec/services/terraform/states/trigger_destroy_service_spec.rb
index 459f4c3bdb9..0b37d962353 100644
--- a/spec/services/terraform/states/trigger_destroy_service_spec.rb
+++ b/spec/services/terraform/states/trigger_destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Terraform::States::TriggerDestroyService do
+RSpec.describe Terraform::States::TriggerDestroyService, feature_category: :infrastructure_as_code do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 13f863dbbdb..31f97edbd08 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TestHooks::ProjectService do
+RSpec.describe TestHooks::ProjectService, feature_category: :code_testing do
include AfterNextHelpers
let(:current_user) { create(:user) }
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index e94ea4669c6..4c5009fea54 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TestHooks::SystemService do
+RSpec.describe TestHooks::SystemService, feature_category: :code_testing do
include AfterNextHelpers
describe '#execute' do
diff --git a/spec/services/timelogs/delete_service_spec.rb b/spec/services/timelogs/delete_service_spec.rb
index ee1133af6b3..c0543bafcec 100644
--- a/spec/services/timelogs/delete_service_spec.rb
+++ b/spec/services/timelogs/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Timelogs::DeleteService do
+RSpec.describe Timelogs::DeleteService, feature_category: :team_planning do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index f73eae70d3c..f0aabd0a8dc 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodoService do
+RSpec.describe TodoService, feature_category: :team_planning do
include AfterNextHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -211,7 +211,6 @@ RSpec.describe TodoService do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:namespace) { project.namespace }
let(:category) { described_class.to_s }
let(:action) { 'incident_management_incident_todo' }
diff --git a/spec/services/todos/allowed_target_filter_service_spec.rb b/spec/services/todos/allowed_target_filter_service_spec.rb
index 1d2b1b044db..3929e3788d0 100644
--- a/spec/services/todos/allowed_target_filter_service_spec.rb
+++ b/spec/services/todos/allowed_target_filter_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::AllowedTargetFilterService do
+RSpec.describe Todos::AllowedTargetFilterService, feature_category: :team_planning do
include DesignManagementTestHelpers
let_it_be(:authorized_group) { create(:group, :private) }
diff --git a/spec/services/todos/destroy/confidential_issue_service_spec.rb b/spec/services/todos/destroy/confidential_issue_service_spec.rb
index e3dcc2bae95..9de71faf8bf 100644
--- a/spec/services/todos/destroy/confidential_issue_service_spec.rb
+++ b/spec/services/todos/destroy/confidential_issue_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::ConfidentialIssueService do
+RSpec.describe Todos::Destroy::ConfidentialIssueService, feature_category: :team_planning do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:author) { create(:user) }
diff --git a/spec/services/todos/destroy/design_service_spec.rb b/spec/services/todos/destroy/design_service_spec.rb
index 92b25d94dc6..628398e7062 100644
--- a/spec/services/todos/destroy/design_service_spec.rb
+++ b/spec/services/todos/destroy/design_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::DesignService do
+RSpec.describe Todos::Destroy::DesignService, feature_category: :design_management do
let_it_be(:user) { create(:user) }
let_it_be(:user_2) { create(:user) }
let_it_be(:design) { create(:design) }
diff --git a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb
index 6d6abe06d1c..63ff189ede5 100644
--- a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb
+++ b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::DestroyedIssuableService do
+RSpec.describe Todos::Destroy::DestroyedIssuableService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/todos/destroy/project_private_service_spec.rb b/spec/services/todos/destroy/project_private_service_spec.rb
index 1d1c010535d..cc15f6eab8b 100644
--- a/spec/services/todos/destroy/project_private_service_spec.rb
+++ b/spec/services/todos/destroy/project_private_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::ProjectPrivateService do
+RSpec.describe Todos::Destroy::ProjectPrivateService, feature_category: :team_planning do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
let(:user) { create(:user) }
diff --git a/spec/services/todos/destroy/unauthorized_features_service_spec.rb b/spec/services/todos/destroy/unauthorized_features_service_spec.rb
index 5f6c9b0cdf0..c02c0dfd5c8 100644
--- a/spec/services/todos/destroy/unauthorized_features_service_spec.rb
+++ b/spec/services/todos/destroy/unauthorized_features_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::UnauthorizedFeaturesService do
+RSpec.describe Todos::Destroy::UnauthorizedFeaturesService, feature_category: :team_planning do
let_it_be(:project, reload: true) { create(:project, :public, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:mr) { create(:merge_request, source_project: project) }
diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb
index 98247250a61..41afabdf2ca 100644
--- a/spec/services/topics/merge_service_spec.rb
+++ b/spec/services/topics/merge_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Topics::MergeService do
+RSpec.describe Topics::MergeService, feature_category: :shared do
let_it_be(:source_topic) { create(:topic, name: 'source_topic') }
let_it_be(:target_topic) { create(:topic, name: 'target_topic') }
let_it_be(:project_1) { create(:project, :public, topic_list: source_topic.name) }
diff --git a/spec/services/two_factor/destroy_service_spec.rb b/spec/services/two_factor/destroy_service_spec.rb
index 30c189520fd..0811ce336c8 100644
--- a/spec/services/two_factor/destroy_service_spec.rb
+++ b/spec/services/two_factor/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TwoFactor::DestroyService do
+RSpec.describe TwoFactor::DestroyService, feature_category: :system_access do
let_it_be(:current_user) { create(:user) }
subject { described_class.new(current_user, user: user).execute }
diff --git a/spec/services/update_container_registry_info_service_spec.rb b/spec/services/update_container_registry_info_service_spec.rb
index 64071e79508..416b08bd04b 100644
--- a/spec/services/update_container_registry_info_service_spec.rb
+++ b/spec/services/update_container_registry_info_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateContainerRegistryInfoService do
+RSpec.describe UpdateContainerRegistryInfoService, feature_category: :container_registry do
let_it_be(:application_settings) { Gitlab::CurrentSettings }
let_it_be(:api_url) { 'http://registry.gitlab' }
diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb
index a07fcee91e4..f30836fbaf5 100644
--- a/spec/services/update_merge_request_metrics_service_spec.rb
+++ b/spec/services/update_merge_request_metrics_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestMetricsService do
+RSpec.describe MergeRequestMetricsService, feature_category: :code_review_workflow do
let(:metrics) { create(:merge_request).metrics }
describe '#merge' do
diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index 48aa65451f3..518d12d5b41 100644
--- a/spec/services/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UploadService do
+RSpec.describe UploadService, feature_category: :shared do
describe 'File service' do
before do
@user = create(:user)
diff --git a/spec/services/uploads/destroy_service_spec.rb b/spec/services/uploads/destroy_service_spec.rb
index bb58da231b6..76ac2ec245e 100644
--- a/spec/services/uploads/destroy_service_spec.rb
+++ b/spec/services/uploads/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Uploads::DestroyService do
+RSpec.describe Uploads::DestroyService, feature_category: :shared do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:upload) { create(:upload, :issuable_upload, model: project) }
diff --git a/spec/services/user_preferences/update_service_spec.rb b/spec/services/user_preferences/update_service_spec.rb
index 59089a4a7af..09acc01729e 100644
--- a/spec/services/user_preferences/update_service_spec.rb
+++ b/spec/services/user_preferences/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserPreferences::UpdateService do
+RSpec.describe UserPreferences::UpdateService, feature_category: :user_profile do
let(:user) { create(:user) }
let(:params) { { view_diffs_file_by_file: false } }
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 356675d55f2..563af8e7e9e 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserProjectAccessChangedService, feature_category: :authentication_and_authorization do
+RSpec.describe UserProjectAccessChangedService, feature_category: :system_access do
describe '#execute' do
it 'permits high-priority operation' do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 6c0d93f568a..f78535569b3 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::ActivityService do
+RSpec.describe Users::ActivityService, feature_category: :user_profile do
include ExclusiveLeaseHelpers
let(:user) { create(:user, last_activity_on: last_activity_on) }
diff --git a/spec/services/users/approve_service_spec.rb b/spec/services/users/approve_service_spec.rb
index 34eb5b18ff6..1b063a9ad1c 100644
--- a/spec/services/users/approve_service_spec.rb
+++ b/spec/services/users/approve_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::ApproveService do
+RSpec.describe Users::ApproveService, feature_category: :user_management do
let_it_be(:current_user) { create(:admin) }
let(:user) { create(:user, :blocked_pending_approval) }
diff --git a/spec/services/users/authorized_build_service_spec.rb b/spec/services/users/authorized_build_service_spec.rb
index 57a122cbf35..7eed6833cba 100644
--- a/spec/services/users/authorized_build_service_spec.rb
+++ b/spec/services/users/authorized_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::AuthorizedBuildService do
+RSpec.describe Users::AuthorizedBuildService, feature_category: :user_management do
describe '#execute' do
let_it_be(:current_user) { create(:user) }
diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb
index 3f9c7ebf067..5be5de82e91 100644
--- a/spec/services/users/ban_service_spec.rb
+++ b/spec/services/users/ban_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::BanService do
+RSpec.describe Users::BanService, feature_category: :user_management do
let(:user) { create(:user) }
let_it_be(:current_user) { create(:admin) }
diff --git a/spec/services/users/banned_user_base_service_spec.rb b/spec/services/users/banned_user_base_service_spec.rb
index 29a549f0f49..65b24e08d80 100644
--- a/spec/services/users/banned_user_base_service_spec.rb
+++ b/spec/services/users/banned_user_base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::BannedUserBaseService do
+RSpec.describe Users::BannedUserBaseService, feature_category: :user_management do
let(:admin) { create(:admin) }
let(:base_service) { described_class.new(admin) }
diff --git a/spec/services/users/batch_status_cleaner_service_spec.rb b/spec/services/users/batch_status_cleaner_service_spec.rb
index 46a004542d8..8feec761fd0 100644
--- a/spec/services/users/batch_status_cleaner_service_spec.rb
+++ b/spec/services/users/batch_status_cleaner_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::BatchStatusCleanerService do
+RSpec.describe Users::BatchStatusCleanerService, feature_category: :user_management do
let_it_be(:user_status_1) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 1.year.ago) }
let_it_be(:user_status_2) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 1.year.from_now) }
let_it_be(:user_status_3) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 2.years.ago) }
diff --git a/spec/services/users/block_service_spec.rb b/spec/services/users/block_service_spec.rb
index 7ff9a887f38..63aa375c8af 100644
--- a/spec/services/users/block_service_spec.rb
+++ b/spec/services/users/block_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::BlockService do
+RSpec.describe Users::BlockService, feature_category: :user_management do
let_it_be(:current_user) { create(:admin) }
subject(:service) { described_class.new(current_user) }
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
index 98fe6d9b5ba..f3236d40412 100644
--- a/spec/services/users/build_service_spec.rb
+++ b/spec/services/users/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::BuildService do
+RSpec.describe Users::BuildService, feature_category: :user_management do
using RSpec::Parameterized::TableSyntax
describe '#execute' do
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index f3c9701c556..eac4faa2042 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::CreateService do
+RSpec.describe Users::CreateService, feature_category: :user_management do
describe '#execute' do
let(:password) { User.random_password }
let(:admin_user) { create(:admin) }
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 18ad946b289..5cd11efe942 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::DestroyService do
+RSpec.describe Users::DestroyService, feature_category: :user_management do
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
let!(:namespace) { user.namespace }
diff --git a/spec/services/users/dismiss_callout_service_spec.rb b/spec/services/users/dismiss_callout_service_spec.rb
index 6ba9f180444..776388ef5f1 100644
--- a/spec/services/users/dismiss_callout_service_spec.rb
+++ b/spec/services/users/dismiss_callout_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::DismissCalloutService do
+RSpec.describe Users::DismissCalloutService, feature_category: :user_management do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/users/dismiss_group_callout_service_spec.rb b/spec/services/users/dismiss_group_callout_service_spec.rb
index d74602a7606..a653fa7ee00 100644
--- a/spec/services/users/dismiss_group_callout_service_spec.rb
+++ b/spec/services/users/dismiss_group_callout_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::DismissGroupCalloutService do
+RSpec.describe Users::DismissGroupCalloutService, feature_category: :user_management do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/users/dismiss_project_callout_service_spec.rb b/spec/services/users/dismiss_project_callout_service_spec.rb
index 73e50a4c37d..7bcb11e4dbc 100644
--- a/spec/services/users/dismiss_project_callout_service_spec.rb
+++ b/spec/services/users/dismiss_project_callout_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::DismissProjectCalloutService do
+RSpec.describe Users::DismissProjectCalloutService, feature_category: :user_management do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/users/email_verification/generate_token_service_spec.rb b/spec/services/users/email_verification/generate_token_service_spec.rb
index e7aa1bf8306..3b299622a90 100644
--- a/spec/services/users/email_verification/generate_token_service_spec.rb
+++ b/spec/services/users/email_verification/generate_token_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::EmailVerification::GenerateTokenService do
+RSpec.describe Users::EmailVerification::GenerateTokenService, feature_category: :system_access do
using RSpec::Parameterized::TableSyntax
let(:service) { described_class.new(attr: attr) }
diff --git a/spec/services/users/email_verification/validate_token_service_spec.rb b/spec/services/users/email_verification/validate_token_service_spec.rb
index 44af4a4d36f..9b69034290a 100644
--- a/spec/services/users/email_verification/validate_token_service_spec.rb
+++ b/spec/services/users/email_verification/validate_token_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::EmailVerification::ValidateTokenService, :clean_gitlab_redis_rate_limiting do
+RSpec.describe Users::EmailVerification::ValidateTokenService, :clean_gitlab_redis_rate_limiting, feature_category: :system_access do
using RSpec::Parameterized::TableSyntax
let(:service) { described_class.new(attr: attr, user: user, token: token) }
diff --git a/spec/services/users/in_product_marketing_email_records_spec.rb b/spec/services/users/in_product_marketing_email_records_spec.rb
index 0b9400dcd12..059f0890b53 100644
--- a/spec/services/users/in_product_marketing_email_records_spec.rb
+++ b/spec/services/users/in_product_marketing_email_records_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::InProductMarketingEmailRecords do
+RSpec.describe Users::InProductMarketingEmailRecords, feature_category: :onboarding do
let_it_be(:user) { create :user }
subject(:records) { described_class.new }
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
index 607d2946b2c..258fe351e4b 100644
--- a/spec/services/users/keys_count_service_spec.rb
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
+RSpec.describe Users::KeysCountService, :use_clean_rails_memory_store_caching, feature_category: :system_access do
let(:user) { create(:user) }
subject { described_class.new(user) }
diff --git a/spec/services/users/last_push_event_service_spec.rb b/spec/services/users/last_push_event_service_spec.rb
index 5b755db407f..fe61f12fe1a 100644
--- a/spec/services/users/last_push_event_service_spec.rb
+++ b/spec/services/users/last_push_event_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::LastPushEventService do
+RSpec.describe Users::LastPushEventService, feature_category: :source_code_management do
let(:user) { build(:user, id: 1) }
let(:project) { build(:project, id: 2) }
let(:event) { build(:push_event, id: 3, author: user, project: project) }
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
index 107ff82016c..0b9f92a868e 100644
--- 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::MigrateRecordsToGhostUserInBatchesService do
+RSpec.describe Users::MigrateRecordsToGhostUserInBatchesService, feature_category: :user_management do
let(:service) { described_class.new }
let_it_be(:ghost_user_migration) { create(:ghost_user_migration) }
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
index 827d6f652a4..cfa0ddff04d 100644
--- a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::MigrateRecordsToGhostUserService do
+RSpec.describe Users::MigrateRecordsToGhostUserService, feature_category: :user_management do
include BatchDestroyDependentAssociationsHelper
let!(:user) { create(:user) }
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index e33886d2add..55b27954a74 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::RefreshAuthorizedProjectsService do
+RSpec.describe Users::RefreshAuthorizedProjectsService, feature_category: :user_management do
include ExclusiveLeaseHelpers
# We're using let! here so that any expectations for the service class are not
diff --git a/spec/services/users/registrations_build_service_spec.rb b/spec/services/users/registrations_build_service_spec.rb
index fa53a4cc604..736db855fe0 100644
--- a/spec/services/users/registrations_build_service_spec.rb
+++ b/spec/services/users/registrations_build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::RegistrationsBuildService do
+RSpec.describe Users::RegistrationsBuildService, feature_category: :system_access do
describe '#execute' do
let(:base_params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
let(:skip_param) { {} }
diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb
index 37d003c5dac..f72666d8a63 100644
--- a/spec/services/users/reject_service_spec.rb
+++ b/spec/services/users/reject_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::RejectService do
+RSpec.describe Users::RejectService, feature_category: :user_management do
let_it_be(:current_user) { create(:admin) }
let(:user) { create(:user, :blocked_pending_approval) }
diff --git a/spec/services/users/repair_ldap_blocked_service_spec.rb b/spec/services/users/repair_ldap_blocked_service_spec.rb
index 54540d68af2..424c14ccdbc 100644
--- a/spec/services/users/repair_ldap_blocked_service_spec.rb
+++ b/spec/services/users/repair_ldap_blocked_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::RepairLdapBlockedService do
+RSpec.describe Users::RepairLdapBlockedService, feature_category: :system_access do
let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
let(:identity) { user.ldap_identity }
diff --git a/spec/services/users/respond_to_terms_service_spec.rb b/spec/services/users/respond_to_terms_service_spec.rb
index 1997dcd0e04..dc33f98535a 100644
--- a/spec/services/users/respond_to_terms_service_spec.rb
+++ b/spec/services/users/respond_to_terms_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::RespondToTermsService do
+RSpec.describe Users::RespondToTermsService, feature_category: :user_profile do
let(:user) { create(:user) }
let(:term) { create(:term) }
diff --git a/spec/services/users/saved_replies/create_service_spec.rb b/spec/services/users/saved_replies/create_service_spec.rb
index e01b6248308..ee42a53a220 100644
--- a/spec/services/users/saved_replies/create_service_spec.rb
+++ b/spec/services/users/saved_replies/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::SavedReplies::CreateService do
+RSpec.describe Users::SavedReplies::CreateService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:current_user) { create(:user) }
let_it_be(:saved_reply) { create(:saved_reply, user: current_user) }
diff --git a/spec/services/users/saved_replies/destroy_service_spec.rb b/spec/services/users/saved_replies/destroy_service_spec.rb
index cb97fac7b7c..41c2013e3df 100644
--- a/spec/services/users/saved_replies/destroy_service_spec.rb
+++ b/spec/services/users/saved_replies/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::SavedReplies::DestroyService do
+RSpec.describe Users::SavedReplies::DestroyService, feature_category: :team_planning do
describe '#execute' do
let!(:saved_reply) { create(:saved_reply) }
diff --git a/spec/services/users/saved_replies/update_service_spec.rb b/spec/services/users/saved_replies/update_service_spec.rb
index bdb54d7c8f7..c18b7395040 100644
--- a/spec/services/users/saved_replies/update_service_spec.rb
+++ b/spec/services/users/saved_replies/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::SavedReplies::UpdateService do
+RSpec.describe Users::SavedReplies::UpdateService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:current_user) { create(:user) }
let_it_be(:saved_reply) { create(:saved_reply, user: current_user) }
diff --git a/spec/services/users/set_status_service_spec.rb b/spec/services/users/set_status_service_spec.rb
index 76e86506d94..b75c558785f 100644
--- a/spec/services/users/set_status_service_spec.rb
+++ b/spec/services/users/set_status_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::SetStatusService do
+RSpec.describe Users::SetStatusService, feature_category: :user_management do
let(:current_user) { create(:user) }
subject(:service) { described_class.new(current_user, params) }
diff --git a/spec/services/users/signup_service_spec.rb b/spec/services/users/signup_service_spec.rb
index ef532e01d0b..29663411346 100644
--- a/spec/services/users/signup_service_spec.rb
+++ b/spec/services/users/signup_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::SignupService do
+RSpec.describe Users::SignupService, feature_category: :system_access do
let(:user) { create(:user, setup_for_company: true) }
describe '#execute' do
@@ -48,11 +48,7 @@ RSpec.describe Users::SignupService do
expect(user.reload.setup_for_company).to be(false)
end
- context 'when on .com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'when on SaaS', :saas do
it 'returns an error result when setup_for_company is missing' do
result = update_user(user, setup_for_company: '')
diff --git a/spec/services/users/unban_service_spec.rb b/spec/services/users/unban_service_spec.rb
index 3dcb8450e7b..20fe40b370f 100644
--- a/spec/services/users/unban_service_spec.rb
+++ b/spec/services/users/unban_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UnbanService do
+RSpec.describe Users::UnbanService, feature_category: :user_management do
let(:user) { create(:user) }
let_it_be(:current_user) { create(:admin) }
diff --git a/spec/services/users/unblock_service_spec.rb b/spec/services/users/unblock_service_spec.rb
index 25ee99427ab..95a077d6100 100644
--- a/spec/services/users/unblock_service_spec.rb
+++ b/spec/services/users/unblock_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UnblockService do
+RSpec.describe Users::UnblockService, feature_category: :user_management do
let_it_be(:current_user) { create(:admin) }
subject(:service) { described_class.new(current_user) }
diff --git a/spec/services/users/update_canonical_email_service_spec.rb b/spec/services/users/update_canonical_email_service_spec.rb
index 1dead13d338..559b759a400 100644
--- a/spec/services/users/update_canonical_email_service_spec.rb
+++ b/spec/services/users/update_canonical_email_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UpdateCanonicalEmailService do
+RSpec.describe Users::UpdateCanonicalEmailService, feature_category: :user_profile do
let(:other_email) { "differentaddress@includeddomain.com" }
before do
diff --git a/spec/services/users/update_highest_member_role_service_spec.rb b/spec/services/users/update_highest_member_role_service_spec.rb
index 89ddd635bb6..06f4d787d72 100644
--- a/spec/services/users/update_highest_member_role_service_spec.rb
+++ b/spec/services/users/update_highest_member_role_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UpdateHighestMemberRoleService do
+RSpec.describe Users::UpdateHighestMemberRoleService, feature_category: :user_management do
let(:user) { create(:user) }
let(:execute_service) { described_class.new(user).execute }
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index f4ea757f81a..1716685566c 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UpdateService do
+RSpec.describe Users::UpdateService, feature_category: :user_profile do
let(:password) { User.random_password }
let(:user) { create(:user, password: password, password_confirmation: password) }
diff --git a/spec/services/users/update_todo_count_cache_service_spec.rb b/spec/services/users/update_todo_count_cache_service_spec.rb
index 3d96af928df..eec637cf5b4 100644
--- a/spec/services/users/update_todo_count_cache_service_spec.rb
+++ b/spec/services/users/update_todo_count_cache_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UpdateTodoCountCacheService do
+RSpec.describe Users::UpdateTodoCountCacheService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index ac7e619612f..37aa5fed838 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UpsertCreditCardValidationService do
+RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user_profile do
let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
let(:user_id) { user.id }
diff --git a/spec/services/users/validate_manual_otp_service_spec.rb b/spec/services/users/validate_manual_otp_service_spec.rb
index d71735814f2..9a6083bc41c 100644
--- a/spec/services/users/validate_manual_otp_service_spec.rb
+++ b/spec/services/users/validate_manual_otp_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::ValidateManualOtpService do
+RSpec.describe Users::ValidateManualOtpService, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
let(:otp_code) { 42 }
@@ -32,6 +32,20 @@ RSpec.describe Users::ValidateManualOtpService do
validate
end
+
+ it 'handles unexpected error' do
+ error_message = "boom!"
+
+ expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::FortiAuthenticator::ManualOtp) do |strategy|
+ expect(strategy).to receive(:validate).with(otp_code).once.and_raise(StandardError, error_message)
+ end
+ expect(Gitlab::ErrorTracking).to receive(:log_exception)
+
+ result = validate
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(error_message)
+ end
end
context 'FortiTokenCloud' do
@@ -49,16 +63,23 @@ RSpec.describe Users::ValidateManualOtpService do
end
end
- context 'unexpected error' do
+ context 'DuoAuth' do
before do
- stub_feature_flags(forti_authenticator: user)
- allow(::Gitlab.config.forti_authenticator).to receive(:enabled).and_return(true)
+ allow(::Gitlab.config.duo_auth).to receive(:enabled).and_return(true)
end
- it 'returns error' do
+ it 'calls DuoAuth strategy' do
+ expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::DuoAuth::ManualOtp) do |strategy|
+ expect(strategy).to receive(:validate).with(otp_code).once
+ end
+
+ validate
+ end
+
+ it "handles unexpected error" do
error_message = "boom!"
- expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::FortiAuthenticator::ManualOtp) do |strategy|
+ expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::DuoAuth::ManualOtp) do |strategy|
expect(strategy).to receive(:validate).with(otp_code).once.and_raise(StandardError, error_message)
end
expect(Gitlab::ErrorTracking).to receive(:log_exception)
diff --git a/spec/services/users/validate_push_otp_service_spec.rb b/spec/services/users/validate_push_otp_service_spec.rb
index 960b6bcd3bb..4ef374cbb7f 100644
--- a/spec/services/users/validate_push_otp_service_spec.rb
+++ b/spec/services/users/validate_push_otp_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::ValidatePushOtpService do
+RSpec.describe Users::ValidatePushOtpService, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
subject(:validate) { described_class.new(user).execute }
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index 42f7ebc85f9..d66d584d3d0 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe VerifyPagesDomainService do
+RSpec.describe VerifyPagesDomainService, feature_category: :pages do
using RSpec::Parameterized::TableSyntax
include EmailHelpers
diff --git a/spec/services/web_hooks/destroy_service_spec.rb b/spec/services/web_hooks/destroy_service_spec.rb
index ca8cb8a1b75..642c25ab312 100644
--- a/spec/services/web_hooks/destroy_service_spec.rb
+++ b/spec/services/web_hooks/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHooks::DestroyService do
+RSpec.describe WebHooks::DestroyService, feature_category: :webhooks do
let_it_be(:user) { create(:user) }
subject { described_class.new(user) }
diff --git a/spec/services/web_hooks/log_destroy_service_spec.rb b/spec/services/web_hooks/log_destroy_service_spec.rb
index 7634726e5a4..b0444b659ba 100644
--- a/spec/services/web_hooks/log_destroy_service_spec.rb
+++ b/spec/services/web_hooks/log_destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHooks::LogDestroyService do
+RSpec.describe WebHooks::LogDestroyService, feature_category: :webhooks do
subject(:service) { described_class.new(hook.id) }
describe '#execute' do
diff --git a/spec/services/web_hooks/log_execution_service_spec.rb b/spec/services/web_hooks/log_execution_service_spec.rb
index 8a845f60ad2..f56c07386fa 100644
--- a/spec/services/web_hooks/log_execution_service_spec.rb
+++ b/spec/services/web_hooks/log_execution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHooks::LogExecutionService do
+RSpec.describe WebHooks::LogExecutionService, feature_category: :webhooks do
include ExclusiveLeaseHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/services/webauthn/authenticate_service_spec.rb b/spec/services/webauthn/authenticate_service_spec.rb
index b40f9465b63..ca940dff0eb 100644
--- a/spec/services/webauthn/authenticate_service_spec.rb
+++ b/spec/services/webauthn/authenticate_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'webauthn/fake_client'
-RSpec.describe Webauthn::AuthenticateService do
+RSpec.describe Webauthn::AuthenticateService, feature_category: :system_access do
let(:client) { WebAuthn::FakeClient.new(origin) }
let(:user) { create(:user) }
let(:challenge) { Base64.strict_encode64(SecureRandom.random_bytes(32)) }
diff --git a/spec/services/webauthn/register_service_spec.rb b/spec/services/webauthn/register_service_spec.rb
index bb9fa2080d2..2286d261e94 100644
--- a/spec/services/webauthn/register_service_spec.rb
+++ b/spec/services/webauthn/register_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'webauthn/fake_client'
-RSpec.describe Webauthn::RegisterService do
+RSpec.describe Webauthn::RegisterService, feature_category: :system_access do
let(:client) { WebAuthn::FakeClient.new(origin) }
let(:user) { create(:user) }
let(:challenge) { Base64.strict_encode64(SecureRandom.random_bytes(32)) }
diff --git a/spec/services/wiki_pages/base_service_spec.rb b/spec/services/wiki_pages/base_service_spec.rb
index 6ccc796014c..f434dc689ef 100644
--- a/spec/services/wiki_pages/base_service_spec.rb
+++ b/spec/services/wiki_pages/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WikiPages::BaseService do
+RSpec.describe WikiPages::BaseService, feature_category: :wiki do
let(:project) { double('project') }
let(:user) { double('user') }
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index fd3776f4207..ca2d38ad70d 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WikiPages::CreateService do
+RSpec.describe WikiPages::CreateService, feature_category: :wiki do
it_behaves_like 'WikiPages::CreateService#execute', :project
describe '#execute' do
diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb
index 9384ea1cd43..ff29fc59b3e 100644
--- a/spec/services/wiki_pages/destroy_service_spec.rb
+++ b/spec/services/wiki_pages/destroy_service_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe WikiPages::DestroyService do
+RSpec.describe WikiPages::DestroyService, feature_category: :wiki do
it_behaves_like 'WikiPages::DestroyService#execute', :project
end
diff --git a/spec/services/wiki_pages/event_create_service_spec.rb b/spec/services/wiki_pages/event_create_service_spec.rb
index 8476f872e98..cbc2bd82a98 100644
--- a/spec/services/wiki_pages/event_create_service_spec.rb
+++ b/spec/services/wiki_pages/event_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WikiPages::EventCreateService do
+RSpec.describe WikiPages::EventCreateService, feature_category: :wiki do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
index 62881817e32..79b2b55907b 100644
--- a/spec/services/wiki_pages/update_service_spec.rb
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WikiPages::UpdateService do
+RSpec.describe WikiPages::UpdateService, feature_category: :wiki do
it_behaves_like 'WikiPages::UpdateService#execute', :project
describe '#execute' do
diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb
index 22e34e1f373..fccdbd3040b 100644
--- a/spec/services/wikis/create_attachment_service_spec.rb
+++ b/spec/services/wikis/create_attachment_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Wikis::CreateAttachmentService do
+RSpec.describe Wikis::CreateAttachmentService, feature_category: :wiki do
let(:container) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:file_name) { 'filename.txt' }
diff --git a/spec/services/work_items/build_service_spec.rb b/spec/services/work_items/build_service_spec.rb
index 405b4414fc2..3ecf78e0659 100644
--- a/spec/services/work_items/build_service_spec.rb
+++ b/spec/services/work_items/build_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::BuildService do
+RSpec.describe WorkItems::BuildService, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/services/work_items/create_from_task_service_spec.rb b/spec/services/work_items/create_from_task_service_spec.rb
index 7c5430f038c..b2f81f1dc54 100644
--- a/spec/services/work_items/create_from_task_service_spec.rb
+++ b/spec/services/work_items/create_from_task_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::CreateFromTaskService do
+RSpec.describe WorkItems::CreateFromTaskService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:list_work_item, refind: true) { create(:work_item, project: project, description: "- [ ] Item to be converted\n second line\n third line") }
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 1b134c308f2..ecd7937f933 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::CreateService do
+RSpec.describe WorkItems::CreateService, feature_category: :team_planning do
include AfterNextHelpers
let_it_be_with_reload(:project) { create(:project) }
diff --git a/spec/services/work_items/delete_service_spec.rb b/spec/services/work_items/delete_service_spec.rb
index 69ae881a12f..ac72815a57e 100644
--- a/spec/services/work_items/delete_service_spec.rb
+++ b/spec/services/work_items/delete_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::DeleteService do
+RSpec.describe WorkItems::DeleteService, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:guest) { create(:user) }
let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: guest) }
diff --git a/spec/services/work_items/delete_task_service_spec.rb b/spec/services/work_items/delete_task_service_spec.rb
index 07a0d8d6c1a..dc01da65771 100644
--- a/spec/services/work_items/delete_task_service_spec.rb
+++ b/spec/services/work_items/delete_task_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::DeleteTaskService do
+RSpec.describe WorkItems::DeleteTaskService, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
let_it_be_with_refind(:task) { create(:work_item, project: project, author: developer) }
diff --git a/spec/services/work_items/export_csv_service_spec.rb b/spec/services/work_items/export_csv_service_spec.rb
index 0718d3b686a..7c22312ce1f 100644
--- a/spec/services/work_items/export_csv_service_spec.rb
+++ b/spec/services/work_items/export_csv_service_spec.rb
@@ -30,9 +30,8 @@ RSpec.describe WorkItems::ExportCsvService, :with_license, feature_category: :te
end
describe '#email' do
- # TODO - will be implemented as part of https://gitlab.com/gitlab-org/gitlab/-/issues/379082
- xit 'emails csv' do
- expect { subject.email(user) }.o change { ActionMailer::Base.deliveries.count }.from(0).to(1)
+ it 'emails csv' do
+ expect { subject.email(user) }.to change { ActionMailer::Base.deliveries.count }.from(0).to(1)
end
end
diff --git a/spec/services/work_items/import_csv_service_spec.rb b/spec/services/work_items/import_csv_service_spec.rb
new file mode 100644
index 00000000000..3c710640f4a
--- /dev/null
+++ b/spec/services/work_items/import_csv_service_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::ImportCsvService, feature_category: :team_planning do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author) { create(:user, username: 'csv_author') }
+ let(:file) { fixture_file_upload('spec/fixtures/work_items_valid_types.csv') }
+ let(:service) do
+ uploader = FileUploader.new(project)
+ uploader.store!(file)
+
+ described_class.new(user, project, uploader)
+ end
+
+ let_it_be(:issue_type) { ::WorkItems::Type.default_issue_type }
+
+ let(:work_items) { ::WorkItems::WorkItemsFinder.new(user, project: project).execute }
+ let(:email_method) { :import_work_items_csv_email }
+
+ subject { service.execute }
+
+ describe '#execute', :aggregate_failures do
+ context 'when user has permission' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'importer with email notification'
+
+ context 'when file format is valid' do
+ context 'when work item types are available' do
+ it 'creates the expected number of work items' do
+ expect { subject }.to change { work_items.count }.by 2
+ end
+
+ it 'sets work item attributes' do
+ result = subject
+
+ expect(work_items.reload).to contain_exactly(
+ have_attributes(
+ title: 'Valid issue',
+ work_item_type_id: issue_type.id
+ ),
+ have_attributes(
+ title: 'Valid issue with alternate case',
+ work_item_type_id: issue_type.id
+ )
+ )
+
+ expect(result[:success]).to eq(2)
+ expect(result[:error_lines]).to eq([])
+ expect(result[:type_errors]).to be_nil
+ expect(result[:parse_error]).to eq(false)
+ end
+ end
+
+ context 'when csv contains work item types that are missing or not available' do
+ let(:file) { fixture_file_upload('spec/fixtures/work_items_invalid_types.csv') }
+
+ it 'creates no work items' do
+ expect { subject }.not_to change { work_items.count }
+ end
+
+ it 'returns the correct result' do
+ result = subject
+
+ expect(result[:success]).to eq(0)
+ expect(result[:error_lines]).to be_empty # there are problematic lines detailed below
+ expect(result[:parse_error]).to eq(false)
+ expect(result[:type_errors]).to match({
+ blank: [4],
+ disallowed: {}, # tested in the EE version
+ missing: {
+ "isssue" => [2],
+ "issue!!" => [3]
+ }
+ })
+ end
+ end
+ end
+
+ context 'when file is missing necessary headers' do
+ let(:file) { fixture_file_upload('spec/fixtures/work_items_missing_header.csv') }
+
+ it 'creates no records' do
+ result = subject
+
+ expect(result[:success]).to eq(0)
+ expect(result[:error_lines]).to eq([1])
+ expect(result[:type_errors]).to be_nil
+ expect(result[:parse_error]).to eq(true)
+ end
+
+ it 'creates no work items' do
+ expect { subject }.not_to change { work_items.count }
+ end
+ end
+
+ context 'when import_export_work_items_csv feature flag is off' do
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(/This feature is currently behind a feature flag and it is not available./)
+ end
+ end
+ end
+
+ context 'when user does not have permission' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(/You do not have permission to import work items in this project/)
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb
index 5884847eac3..a989ecf9c07 100644
--- a/spec/services/work_items/parent_links/create_service_spec.rb
+++ b/spec/services/work_items/parent_links/create_service_spec.rb
@@ -68,6 +68,40 @@ RSpec.describe WorkItems::ParentLinks::CreateService, feature_category: :portfol
end
end
+ context 'when adjacent is already in place' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:parent_item) { create(:work_item, :objective, project: project) }
+ let_it_be_with_reload(:current_item) { create(:work_item, :objective, project: project) }
+
+ let_it_be_with_reload(:adjacent) do
+ create(:work_item, :objective, project: project)
+ end
+
+ let_it_be_with_reload(:link_to_adjacent) do
+ create(:parent_link, work_item_parent: parent_item, work_item: adjacent)
+ end
+
+ subject { described_class.new(parent_item, user, { target_issuable: current_item }).execute }
+
+ where(:adjacent_position, :expected_order) do
+ -100 | lazy { [adjacent, current_item] }
+ 0 | lazy { [adjacent, current_item] }
+ 100 | lazy { [adjacent, current_item] }
+ end
+
+ with_them do
+ before do
+ link_to_adjacent.update!(relative_position: adjacent_position)
+ end
+
+ it 'sets relative positions' do
+ expect { subject }.to change(parent_link_class, :count).by(1)
+ expect(parent_item.work_item_children_by_relative_position).to eq(expected_order)
+ end
+ end
+ end
+
context 'when there are tasks to relate' do
let(:params) { { issuable_references: [task1, task2] } }
diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb
index 654a03ef6f7..c77546f6ca1 100644
--- a/spec/services/work_items/parent_links/destroy_service_spec.rb
+++ b/spec/services/work_items/parent_links/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::ParentLinks::DestroyService do
+RSpec.describe WorkItems::ParentLinks::DestroyService, feature_category: :team_planning do
describe '#execute' do
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/services/work_items/task_list_reference_removal_service_spec.rb b/spec/services/work_items/task_list_reference_removal_service_spec.rb
index 91b7814ae92..4e87ce66c21 100644
--- a/spec/services/work_items/task_list_reference_removal_service_spec.rb
+++ b/spec/services/work_items/task_list_reference_removal_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::TaskListReferenceRemovalService do
+RSpec.describe WorkItems::TaskListReferenceRemovalService, feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository).tap { |project| project.add_developer(developer) } }
let_it_be(:task) { create(:work_item, project: project, title: 'Task title') }
diff --git a/spec/services/work_items/task_list_reference_replacement_service_spec.rb b/spec/services/work_items/task_list_reference_replacement_service_spec.rb
index 965c5f1d554..8f696109fa1 100644
--- a/spec/services/work_items/task_list_reference_replacement_service_spec.rb
+++ b/spec/services/work_items/task_list_reference_replacement_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::TaskListReferenceReplacementService do
+RSpec.describe WorkItems::TaskListReferenceReplacementService, feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository).tap { |project| project.add_developer(developer) } }
let_it_be(:single_line_work_item, refind: true) { create(:work_item, project: project, description: '- [ ] single line', lock_version: 3) }
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 435995c6570..5647f8c085c 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::UpdateService do
+RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
index 0ab2c85f078..67736592876 100644
--- a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time do
+RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time, feature_category: :portfolio_management do
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:new_assignee) { create(:user) }
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 4275950e720..20b5758dde9 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService do
+RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService, feature_category: :portfolio_management do
let_it_be(:random_user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:guest) { create(:user) }
diff --git a/spec/services/work_items/widgets/hierarchy_service/create_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/create_service_spec.rb
new file mode 100644
index 00000000000..8d834c9a4f8
--- /dev/null
+++ b/spec/services/work_items/widgets/hierarchy_service/create_service_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::HierarchyService::CreateService, feature_category: :portfolio_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:parent_item) { create(:work_item, project: project) }
+
+ let(:widget) { parent_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Hierarchy) } }
+
+ shared_examples 'raises a WidgetError' do
+ it { expect { subject }.to raise_error(described_class::WidgetError, message) }
+ end
+
+ before(:all) do
+ project.add_developer(user)
+ end
+
+ describe '#create' do
+ subject { described_class.new(widget: widget, current_user: user).after_create_in_transaction(params: params) }
+
+ context 'when invalid params are present' do
+ let(:params) { { other_parent: 'parent_work_item' } }
+
+ it_behaves_like 'raises a WidgetError' do
+ let(:message) { 'One or more arguments are invalid: other_parent.' }
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
index 3f90784b703..64ab2421c74 100644
--- a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
+++ b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::MilestoneService::CreateService do
+RSpec.describe WorkItems::Widgets::MilestoneService::CreateService, feature_category: :portfolio_management do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:project_milestone) { create(:milestone, project: project) }
diff --git a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
index f3a7fc156b9..c5bc2b12fc5 100644
--- a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::MilestoneService::UpdateService do
+RSpec.describe WorkItems::Widgets::MilestoneService::UpdateService, feature_category: :portfolio_management do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:project_milestone) { create(:milestone, project: project) }
diff --git a/spec/services/work_items/widgets/notifications_service/update_service_spec.rb b/spec/services/work_items/widgets/notifications_service/update_service_spec.rb
new file mode 100644
index 00000000000..9615020fe49
--- /dev/null
+++ b/spec/services/work_items/widgets/notifications_service/update_service_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::NotificationsService::UpdateService, feature_category: :team_planning do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:author) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be_with_reload(:work_item) { create(:work_item, project: project, author: author) }
+ let_it_be(:current_user) { guest }
+
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Notifications) } }
+ let(:service) { described_class.new(widget: widget, current_user: current_user) }
+
+ describe '#before_update_in_transaction' do
+ let(:expected) { params[:subscribed] }
+
+ subject(:update_notifications) { service.before_update_in_transaction(params: params) }
+
+ shared_examples 'failing to update subscription' do
+ context 'when user is subscribed with a subscription record' do
+ let_it_be(:subscription) { create_subscription(:subscribed) }
+
+ it "does not update the work item's subscription" do
+ expect do
+ update_notifications
+ subscription.reload
+ end.to not_change { subscription.subscribed }
+ .and(not_change { work_item.subscribed?(current_user, project) })
+ end
+ end
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'does not create subscription record or change subscription state' do
+ expect { update_notifications }
+ .to not_change { Subscription.count }
+ .and(not_change { work_item.subscribed?(current_user, project) })
+ end
+ end
+ end
+
+ shared_examples 'updating notifications subscription successfully' do
+ it 'updates existing subscription record' do
+ expect do
+ update_notifications
+ subscription.reload
+ end.to change { subscription.subscribed }.to(expected)
+ .and(change { work_item.subscribed?(current_user, project) }.to(expected))
+ end
+ end
+
+ context 'when update fails' do
+ context 'when user lack update_subscription permissions' do
+ let_it_be(:params) { { subscribed: false } }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :update_subscription, work_item)
+ .and_return(false)
+ end
+
+ it_behaves_like 'failing to update subscription'
+ end
+
+ context 'when notifications params are not present' do
+ let_it_be(:params) { {} }
+
+ it_behaves_like 'failing to update subscription'
+ end
+ end
+
+ context 'when update is successful' do
+ context 'when subscribing' do
+ let_it_be(:subscription) { create_subscription(:unsubscribed) }
+ let(:params) { { subscribed: true } }
+
+ it_behaves_like 'updating notifications subscription successfully'
+ end
+
+ context 'when unsubscribing' do
+ let(:params) { { subscribed: false } }
+
+ context 'when user is subscribed with a subscription record' do
+ let_it_be(:subscription) { create_subscription(:subscribed) }
+
+ it_behaves_like 'updating notifications subscription successfully'
+ end
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'creates a subscription with expected value' do
+ expect { update_notifications }
+ .to change { Subscription.count }.by(1)
+ .and(change { work_item.subscribed?(current_user, project) }.to(expected))
+
+ expect(Subscription.last.subscribed).to eq(expected)
+ end
+ end
+ end
+ end
+ end
+
+ def create_subscription(state)
+ create(
+ :subscription,
+ project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: (state == :subscribed)
+ )
+ end
+end
diff --git a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb
index d328c541fc7..a46e9ac9f7a 100644
--- a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService do
+RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService, feature_category: :portfolio_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:work_item) { create(:work_item, project: project) }
diff --git a/spec/services/x509_certificate_revoke_service_spec.rb b/spec/services/x509_certificate_revoke_service_spec.rb
index ff5d2dc058b..460381afd79 100644
--- a/spec/services/x509_certificate_revoke_service_spec.rb
+++ b/spec/services/x509_certificate_revoke_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe X509CertificateRevokeService do
+RSpec.describe X509CertificateRevokeService, feature_category: :system_access do
describe '#execute' do
let(:service) { described_class.new }
let!(:x509_signature_1) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified) }
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index 07a23021ef5..bea312369f7 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -10,6 +10,7 @@ module SimpleCovEnv
def start!
return if !ENV.key?('SIMPLECOV') || ENV['SIMPLECOV'] == '0'
+ return if SimpleCov.running
configure_profile
configure_job
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4e8f990fc10..3b50d821b4c 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -7,9 +7,7 @@ if $LOADED_FEATURES.include?(File.expand_path('fast_spec_helper.rb', __dir__))
abort 'Aborting...'
end
-# Enable deprecation warnings by default and make them more visible
-# to developers to ease upgrading to newer Ruby versions.
-Warning[:deprecated] = true unless ENV.key?('SILENCE_DEPRECATIONS')
+require './spec/deprecation_warnings'
require './spec/deprecation_toolkit_env'
DeprecationToolkitEnv.configure!
@@ -38,6 +36,7 @@ require 'test_prof/recipes/rspec/let_it_be'
require 'test_prof/factory_default'
require 'test_prof/factory_prof/nate_heckler'
require 'parslet/rig/rspec'
+require 'axe-rspec'
rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
@@ -178,6 +177,8 @@ RSpec.configure do |config|
config.include RenderedHelpers
config.include RSpec::Benchmark::Matchers, type: :benchmark
config.include DetailedErrorHelpers
+ config.include RequestUrgencyMatcher, type: :controller
+ config.include RequestUrgencyMatcher, type: :request
config.include_context 'when rendered has no HTML escapes', type: :view
@@ -356,35 +357,14 @@ RSpec.configure do |config|
# All API specs will be adapted continuously. The following list contains the specs that have not yet been adapted.
# The feature flag is disabled for these specs as long as they are not yet adapted.
admin_mode_for_api_feature_flag_paths = %w[
- ./spec/frontend/fixtures/api_deploy_keys.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/broadcast_messages_spec.rb
- ./spec/requests/api/ci/pipelines_spec.rb
- ./spec/requests/api/ci/runners_reset_registration_token_spec.rb
- ./spec/requests/api/ci/runners_spec.rb
./spec/requests/api/deploy_keys_spec.rb
./spec/requests/api/deploy_tokens_spec.rb
- ./spec/requests/api/freeze_periods_spec.rb
- ./spec/requests/api/graphql/user/starred_projects_query_spec.rb
./spec/requests/api/groups_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/merge_requests_spec.rb
./spec/requests/api/namespaces_spec.rb
./spec/requests/api/notes_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/pages_domains_spec.rb
./spec/requests/api/personal_access_tokens/self_information_spec.rb
./spec/requests/api/personal_access_tokens_spec.rb
./spec/requests/api/project_export_spec.rb
@@ -544,19 +524,17 @@ RSpec.configure do |config|
end
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|
- c.max_formatted_output_length = nil
- end
- end
-
# Ensures that any Javascript script that tries to make the external VersionCheck API call skips it and returns a response
config.before(:each, :js) do
allow_any_instance_of(VersionCheck).to receive(:response).and_return({ "severity" => "success" })
end
end
+# Disabled because it's causing N+1 queries.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/396352.
+# Support::AbilityCheck.inject(Ability.singleton_class)
+Support::PermissionsCheck.inject(Ability.singleton_class)
+
ActiveRecord::Migration.maintain_test_schema!
Shoulda::Matchers.configure do |config|
diff --git a/spec/support/ability_check.rb b/spec/support/ability_check.rb
new file mode 100644
index 00000000000..213944506bb
--- /dev/null
+++ b/spec/support/ability_check.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'gitlab/utils/strong_memoize'
+
+module Support
+ module AbilityCheck
+ def self.inject(mod)
+ mod.prepend AbilityExtension
+ end
+
+ module AbilityExtension
+ def before_check(policy, ability, user, subject, opts)
+ return super if Checker.ok?(policy, ability)
+
+ ActiveSupport::Deprecation.warn(<<~WARNING)
+ Ability #{ability.inspect} in #{policy.class} not found.
+ user=#{user.inspect}, subject=#{subject}, opts=#{opts.inspect}"
+
+ To exclude this check add this entry to #{Checker::TODO_YAML}:
+ #{policy.class}:
+ - #{ability}
+ WARNING
+ end
+ end
+
+ module Checker
+ include Gitlab::Utils::StrongMemoize
+ extend self
+
+ TODO_YAML = File.join(__dir__, 'ability_check_todo.yml')
+
+ def ok?(policy, ability)
+ ignored?(policy, ability) || ability_found?(policy, ability)
+ end
+
+ private
+
+ def ignored?(policy, ability)
+ todo_list[policy.class.name]&.include?(ability.to_s)
+ end
+
+ # Use Policy#has_ability? instead after it has been accepted and released.
+ # See https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/issues/25
+ def ability_found?(policy, ability)
+ # NilPolicy has no abilities. Ignore it.
+ return true if policy.is_a?(DeclarativePolicy::NilPolicy)
+
+ # Search in current policy first
+ return true if policy.class.ability_map.map.key?(ability)
+
+ # Search recursively in all delegations otherwise.
+ # This is potentially slow.
+ # Stolen from:
+ # https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/d691e/lib/declarative_policy/base.rb#L360-369
+ policy.class.delegations.any? do |_, block|
+ new_subject = policy.instance_eval(&block)
+ new_policy = policy.policy_for(new_subject)
+
+ ability_found?(new_policy, ability)
+ end
+ end
+
+ def todo_list
+ hash = YAML.load_file(TODO_YAML)
+ return {} unless hash.is_a?(Hash)
+
+ hash.transform_values(&:to_set)
+ end
+
+ strong_memoize_attr :todo_list
+ end
+ end
+end
diff --git a/spec/support/ability_check_todo.yml b/spec/support/ability_check_todo.yml
new file mode 100644
index 00000000000..eafd595b137
--- /dev/null
+++ b/spec/support/ability_check_todo.yml
@@ -0,0 +1,73 @@
+# This list tracks unknown abilities per policy.
+#
+# This file is used by `spec/support/ability_check.rb`.
+#
+# Each TODO entry means that an ability wasn't found in
+# the particular policy class or its delegations.
+#
+# This could be one of the reasons:
+# * The ability is misspelled.
+# - Suggested action: Fix typo.
+# * The ability has been removed from a policy but is still in use.
+# - Remove production code in question.
+# * The ability is defined in EE policy but is used in FOSS code.
+# - Guard the check or move it to EE folder.
+# - See https://docs.gitlab.com/ee/development/ee_features.html
+# * The ability is defined in another policy but delegation is missing.
+# - Add delegation policy or guard the check with a type check.
+# - See https://docs.gitlab.com/ee/development/policies.html#delegation
+# * The ability check is polymorphic (for example, Issuable) and some policies
+# do not implement this ability.
+# - Exclude TODO permanently below.
+# - Guard the check with a type check.
+# * The ability check is defined on GraphQL field which does not support
+# authorization on resolved field values yet.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/300922
+---
+# <Policy class>:
+# - <ability name>
+# - <ability name>
+# ...
+
+# Temporary excludes:
+
+Ci::BridgePolicy:
+- read_job_artifacts
+CommitStatusPolicy:
+- read_job_artifacts
+EpicPolicy:
+- create_timelog
+- read_emoji
+- set_issue_crm_contacts
+GlobalPolicy:
+- read_achievement
+- read_on_demand_dast_scan
+- update_max_pages_size
+GroupPolicy:
+- admin_merge_request
+- change_push_rules
+- manage_owners
+IssuePolicy:
+- create_test_case
+MergeRequestPolicy:
+- set_confidentiality
+- set_issue_crm_contacts
+Namespaces::UserNamespacePolicy:
+- read_crm_contact
+PersonalSnippetPolicy:
+- read_internal_note
+- read_project
+ProjectMemberPolicy:
+- override_project_member
+ProjectPolicy:
+- admin_feature_flags_issue_links
+- admin_vulnerability
+- create_requirement
+- create_test_case
+- read_group_saml_identity
+UserPolicy:
+- admin_observability
+- admin_terraform_state
+- read_observability
+
+# Permanent excludes (please provide a reason):
diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb
index cd3ab4bd82d..1818dec5fc7 100644
--- a/spec/support/helpers/ci/template_helpers.rb
+++ b/spec/support/helpers/ci/template_helpers.rb
@@ -6,6 +6,10 @@ module Ci
'registry.gitlab.com'
end
+ def auto_build_image_repository
+ "gitlab-org/cluster-integration/auto-build-image"
+ end
+
def public_image_exist?(registry, repository, image)
public_image_manifest(registry, repository, image).present?
end
diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb
new file mode 100644
index 00000000000..c12fd1fbbd7
--- /dev/null
+++ b/spec/support/helpers/content_editor_helpers.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module ContentEditorHelpers
+ def switch_to_content_editor
+ click_button _('Viewing markdown')
+ click_button _('Rich text')
+ 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
+
+ def wait_until_hidden_field_is_updated(value)
+ expect(page).to have_field(with: value, type: 'hidden')
+ end
+
+ def display_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
+ end
+
+ def click_edit_diagram_button
+ page.find('[data-testid="edit-diagram"]').click
+ end
+
+ def expect_drawio_editor_is_opened
+ expect(page).to have_css('#drawio-frame', visible: :hidden)
+ end
+end
diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
deleted file mode 100644
index 2ed1222ebd3..00000000000
--- a/spec/support/helpers/fake_u2f_device.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-class FakeU2fDevice
- attr_reader :name
-
- def initialize(page, name, device = nil)
- @page = page
- @name = name
- @u2f_device = device
- end
-
- def respond_to_u2f_registration
- app_id = @page.evaluate_script('gon.u2f.app_id')
- challenges = @page.evaluate_script('gon.u2f.challenges')
-
- json_response = u2f_device(app_id).register_response(challenges[0])
-
- @page.execute_script("
- u2f.register = function(appId, registerRequests, signRequests, callback) {
- callback(#{json_response});
- };
- ")
- end
-
- def respond_to_u2f_authentication
- app_id = @page.evaluate_script('gon.u2f.app_id')
- challenge = @page.evaluate_script('gon.u2f.challenge')
- json_response = u2f_device(app_id).sign_response(challenge)
-
- @page.execute_script("
- u2f.sign = function(appId, challenges, signRequests, callback) {
- callback(#{json_response});
- };
- window.gl.u2fAuthenticate.start();
- ")
- end
-
- def fake_u2f_authentication
- @page.execute_script("window.gl.u2fAuthenticate.renderAuthenticated('abc');")
- end
-
- private
-
- def u2f_device(app_id)
- @u2f_device ||= U2F::FakeU2F.new(app_id)
- end
-end
diff --git a/spec/support/helpers/features/two_factor_helpers.rb b/spec/support/helpers/features/two_factor_helpers.rb
index 08a7665201f..d5f069a40ea 100644
--- a/spec/support/helpers/features/two_factor_helpers.rb
+++ b/spec/support/helpers/features/two_factor_helpers.rb
@@ -14,23 +14,25 @@ module Spec
module Helpers
module Features
module TwoFactorHelpers
+ def copy_recovery_codes
+ click_on _('Copy codes')
+ click_on _('Proceed')
+ end
+
+ def enable_two_factor_authentication
+ click_on _('Enable two-factor authentication')
+ expect(page).to have_content(_('Set up new device'))
+ wait_for_requests
+ end
+
def manage_two_factor_authentication
click_on 'Manage two-factor authentication'
expect(page).to have_content("Set up new device")
wait_for_requests
end
- def register_u2f_device(u2f_device = nil, name: 'My device')
- u2f_device ||= FakeU2fDevice.new(page, name)
- u2f_device.respond_to_u2f_registration
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- fill_in "Pick a name", with: name
- click_on 'Register device'
- u2f_device
- end
-
# Registers webauthn device via UI
+ # Remove after `webauthn_without_totp` feature flag is deleted.
def register_webauthn_device(webauthn_device = nil, name: 'My device')
webauthn_device ||= FakeWebauthnDevice.new(page, name)
webauthn_device.respond_to_webauthn_registration
@@ -41,6 +43,25 @@ module Spec
webauthn_device
end
+ def webauthn_device_registration(webauthn_device: nil, name: 'My device', password: 'fake')
+ webauthn_device ||= FakeWebauthnDevice.new(page, name)
+ webauthn_device.respond_to_webauthn_registration
+ click_on _('Set up new device')
+ webauthn_fill_form_and_submit(name: name, password: password)
+ webauthn_device
+ end
+
+ def webauthn_fill_form_and_submit(name: 'My device', password: 'fake')
+ expect(page).to have_content(
+ _('Your device was successfully set up! Give it a name and register it with the GitLab server.')
+ )
+ within '[data-testid="create-webauthn"]' do
+ fill_in _('Device name'), with: name
+ fill_in _('Current password'), with: password
+ click_on _('Register device')
+ end
+ end
+
# Adds webauthn device directly via database
def add_webauthn_device(app_id, user, fake_device = nil, name: 'My device')
fake_device ||= WebAuthn::FakeClient.new(app_id)
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 677cea7b804..b07f5dcf2e1 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -190,9 +190,9 @@ module FilteredSearchHelpers
##
# For use with gl-filtered-search
- def select_tokens(*args, submit: false)
+ def select_tokens(*args, submit: false, input_text: 'Search')
within '[data-testid="filtered-search-input"]' do
- find_field('Search').click
+ find_field(input_text).click
args.each do |token|
# Move mouse away to prevent invoking tooltips on usernames, which blocks the search input
@@ -230,6 +230,13 @@ module FilteredSearchHelpers
find('.gl-filtered-search-token-segment', text: value).click
end
+ def toggle_sort_direction
+ page.within('.vue-filtered-search-bar-container .sort-dropdown-container') do
+ page.find("button[title^='Sort direction']").click
+ wait_for_requests
+ end
+ end
+
def expect_visible_suggestions_list
expect(page).to have_css('.gl-filtered-search-suggestion-list')
end
diff --git a/spec/support/helpers/fixture_helpers.rb b/spec/support/helpers/fixture_helpers.rb
index 7b3b8ae5f7a..eafdecb2e3d 100644
--- a/spec/support/helpers/fixture_helpers.rb
+++ b/spec/support/helpers/fixture_helpers.rb
@@ -8,6 +8,6 @@ module FixtureHelpers
end
def expand_fixture_path(filename, dir: '')
- File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
+ File.expand_path(rails_root_join(dir, 'spec', 'fixtures', filename))
end
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 398a2a20f2f..bf3c67a1818 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -198,8 +198,23 @@ module GitalySetup
end
LOGGER.debug "Checking gitaly-ruby bundle...\n"
+
+ bundle_install unless bundle_check
+
+ abort 'bundle check failed' unless bundle_check
+ end
+
+ def bundle_check
+ bundle_cmd('check')
+ end
+
+ def bundle_install
+ bundle_cmd('install')
+ end
+
+ def bundle_cmd(cmd)
out = ENV['CI'] ? $stdout : '/dev/null'
- abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: gemfile_dir)
+ system(env, 'bundle', cmd, out: out, chdir: gemfile_dir)
end
def connect_proc(toml)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 2176a477371..191e5192a61 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -310,7 +310,7 @@ module GraphqlHelpers
"{ #{q} }"
end
- def graphql_mutation(name, input, fields = nil, &block)
+ def graphql_mutation(name, input, fields = nil, excluded = [], &block)
raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block
name = name.graphql_name if name.respond_to?(:graphql_name)
@@ -319,7 +319,7 @@ module GraphqlHelpers
mutation_field = GitlabSchema.mutation.fields[mutation_name]
fields = yield if block
- fields ||= all_graphql_fields_for(mutation_field.type.to_type_signature)
+ fields ||= all_graphql_fields_for(mutation_field.type.to_type_signature, excluded: excluded)
query = <<~MUTATION
mutation(#{input_variable_name}: #{mutation_field.arguments['input'].type.to_type_signature}) {
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 5fde80e6dc9..e93d04a0b80 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -139,11 +139,6 @@ module LoginHelpers
click_link_or_button "oauth-login-#{provider}"
end
- def fake_successful_u2f_authentication
- allow(U2fRegistration).to receive(:authenticate).and_return(true)
- FakeU2fDevice.new(page, nil).fake_u2f_authentication
- end
-
def fake_successful_webauthn_authentication
allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
FakeWebauthnDevice.new(page, nil).fake_webauthn_authentication
@@ -218,6 +213,15 @@ module LoginHelpers
config
end
+ def prepare_provider_route(provider_name)
+ routes = Rails.application.routes
+ routes.disable_clear_and_finalize = true
+ routes.formatter.clear
+ routes.draw do
+ post "/users/auth/#{provider_name}" => "omniauth_callbacks##{provider_name}"
+ end
+ end
+
def stub_omniauth_provider(provider, context: Rails.application)
env = env_from_context(context)
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index 48c6e590e1b..8248ea0bb84 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -106,6 +106,14 @@ module NavbarStructureHelper
)
end
+ def insert_infrastructure_aws_nav
+ insert_after_sub_nav_item(
+ _('Google Cloud'),
+ within: _('Infrastructure'),
+ new_sub_nav_item_name: _('AWS')
+ )
+ end
+
def project_analytics_sub_nav_item
[
_('Value stream'),
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index 5be9ba9ae1e..e8fa73a1b95 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -102,6 +102,10 @@ module ActiveRecord
@occurrences ||= @log.group_by(&:to_s).transform_values(&:count)
end
+ def occurrences_starting_with(str)
+ occurrences.select { |query, _count| query.starts_with?(str) }
+ end
+
def ignorable?(values)
return true if skip_schema_queries && values[:name]&.include?("SCHEMA")
return true if values[:name]&.match(/License Load/)
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 9f37cf61cc9..45467fb7099 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -41,6 +41,7 @@ eos
line_code: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14',
line_code_path: 'files/ruby/popen.rb',
del_line_code: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13',
+ referenced_by: [],
message: <<eos
Change some files
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
@@ -56,6 +57,7 @@ eos
author_full_name: "Sytse Sijbrandij",
author_email: "sytse@gitlab.com",
files_changed_count: 1,
+ referenced_by: [],
message: <<eos
Add directory structure for tree_helper spec
@@ -74,6 +76,7 @@ eos
sha: "913c66a37b4a45b9769037c55c2d238bd0942d2e",
author_full_name: "Dmitriy Zaporozhets",
author_email: "dmitriy.zaporozhets@gmail.com",
+ referenced_by: [],
message: <<eos
Files, encoding and much more
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
@@ -89,6 +92,7 @@ eos
author_email: "dmitriy.zaporozhets@gmail.com",
old_blob_id: '33f3729a45c02fc67d00adb1b8bca394b0e761d9',
new_blob_id: '2f63565e7aac07bcdadb654e253078b727143ec4',
+ referenced_by: [],
message: <<eos
Modified image
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index 4ca8f26be9e..2a7b36a4c00 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -179,7 +179,7 @@ module StubConfiguration
def to_settings(hash)
hash.transform_values do |value|
if value.is_a? Hash
- Settingslogic.new(value.deep_stringify_keys)
+ Settingslogic.new(value.to_h.deep_stringify_keys)
else
value
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 6b633856228..d120e1805e3 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -15,7 +15,7 @@ module StubObjectStorage
direct_upload: false,
cdn: {}
)
- old_config = Settingslogic.new(config.deep_stringify_keys)
+ old_config = Settingslogic.new(config.to_h.deep_stringify_keys)
new_config = config.to_h.deep_symbolize_keys.merge({
enabled: enabled,
proxy_download: proxy_download,
@@ -30,7 +30,7 @@ module StubObjectStorage
allow(config).to receive(:proxy_download) { proxy_download }
allow(config).to receive(:direct_upload) { direct_upload }
- uploader_config = Settingslogic.new(new_config.deep_stringify_keys)
+ uploader_config = Settingslogic.new(new_config.to_h.deep_stringify_keys)
allow(uploader).to receive(:object_store_options).and_return(uploader_config)
allow(uploader.options).to receive(:object_store).and_return(uploader_config)
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 3403064bf0b..727b8a6b880 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -2,6 +2,7 @@
require 'parallel'
require_relative 'gitaly_setup'
+require_relative '../../../lib/gitlab/setup_helper'
module TestEnv
extend self
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 2bec945fbc8..9abfc39e31f 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -54,8 +54,6 @@ module UsageDataHelpers
projects_asana_active
projects_jenkins_active
projects_jira_active
- projects_jira_server_active
- projects_jira_cloud_active
projects_jira_dvcs_cloud_active
projects_jira_dvcs_server_active
projects_slack_active
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index 9f39f576b95..97993b158c8 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -100,3 +100,17 @@ RSpec::Matchers.define :be_finalize_background_migration_of do |migration|
end
end
end
+
+RSpec::Matchers.define :ensure_batched_background_migration_is_finished_for do |migration_arguments|
+ define_method :matches? do |klass|
+ expect_next_instance_of(klass) do |instance|
+ expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)
+ end
+ end
+
+ define_method :does_not_match? do |klass|
+ expect_next_instance_of(klass) do |instance|
+ expect(instance).not_to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments)
+ end
+ end
+end
diff --git a/spec/support/matchers/exceed_redis_call_limit.rb b/spec/support/matchers/exceed_redis_call_limit.rb
new file mode 100644
index 00000000000..2b1e1ebad23
--- /dev/null
+++ b/spec/support/matchers/exceed_redis_call_limit.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module ExceedRedisCallLimitHelpers
+ def build_recorder(block)
+ return block if block.is_a?(RedisCommands::Recorder)
+
+ RedisCommands::Recorder.new(&block)
+ end
+
+ def verify_count(expected, block)
+ @actual = build_recorder(block).count
+
+ @actual > expected
+ end
+
+ def verify_commands_count(command, expected, block)
+ @actual = build_recorder(block).by_command(command).count
+
+ @actual > expected
+ end
+end
+
+RSpec::Matchers.define :exceed_redis_calls_limit do |expected|
+ supports_block_expectations
+
+ include ExceedRedisCallLimitHelpers
+
+ match do |block|
+ verify_count(expected, block)
+ end
+
+ failure_message do
+ "Expected at least #{expected} calls, but got #{actual}"
+ end
+
+ failure_message_when_negated do
+ "Expected a maximum of #{expected} calls, but got #{actual}"
+ end
+end
+
+RSpec::Matchers.define :exceed_redis_command_calls_limit do |command, expected|
+ supports_block_expectations
+
+ include ExceedRedisCallLimitHelpers
+
+ match do |block|
+ verify_commands_count(command, expected, block)
+ end
+
+ failure_message do
+ "Expected at least #{expected} calls to '#{command}', but got #{actual}"
+ end
+
+ failure_message_when_negated do
+ "Expected a maximum of #{expected} calls to '#{command}', but got #{actual}"
+ end
+end
diff --git a/spec/support/matchers/request_urgency_matcher.rb b/spec/support/matchers/request_urgency_matcher.rb
new file mode 100644
index 00000000000..d3c5093719e
--- /dev/null
+++ b/spec/support/matchers/request_urgency_matcher.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+module RequestUrgencyMatcher
+ RSpec::Matchers.define :have_request_urgency do |request_urgency|
+ match do |_actual|
+ if controller_instance = request.env["action_controller.instance"]
+ controller_instance.urgency.name == request_urgency
+ elsif endpoint = request.env['api.endpoint']
+ urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
+ urgency.name == request_urgency
+ else
+ raise 'neither a controller nor a request spec'
+ end
+ end
+
+ failure_message do |_actual|
+ if controller_instance = request.env["action_controller.instance"]
+ "request urgency #{controller_instance.urgency.name} is set, \
+ but expected to be #{request_urgency}".squish
+ elsif endpoint = request.env['api.endpoint']
+ urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
+ "request urgency #{urgency.name} is set, \
+ but expected to be #{request_urgency}".squish
+ end
+ end
+ end
+end
diff --git a/spec/support/permissions_check.rb b/spec/support/permissions_check.rb
new file mode 100644
index 00000000000..efe0ecb530b
--- /dev/null
+++ b/spec/support/permissions_check.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Support
+ module PermissionsCheck
+ def self.inject(mod)
+ mod.prepend PermissionsExtension if Gitlab::Utils.to_boolean(ENV['GITLAB_DEBUG_POLICIES'])
+ end
+
+ module PermissionsExtension
+ def before_check(policy, ability, _user, _subject, _opts)
+ puts(
+ "POLICY CHECK DEBUG -> " \
+ "policy: #{policy.class.name}, ability: #{ability}, called_from: #{caller_locations(2, 5)}"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb
index 8666c19481c..6aa9647bcec 100644
--- a/spec/support/protected_tags/access_control_ce_shared_examples.rb
+++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb
@@ -6,18 +6,8 @@ RSpec.shared_examples "protected tags > access control > CE" do
visit project_protected_tags_path(project)
set_protected_tag_name('master')
-
- within('.js-new-protected-tag') do
- allowed_to_create_button = find(".js-allowed-to-create")
-
- unless allowed_to_create_button.text == access_type_name
- allowed_to_create_button.click
- find('.create_access_levels-container .dropdown-menu li', match: :first)
- within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
- end
- end
-
- click_on "Protect"
+ set_allowed_to('create', access_type_name)
+ click_on_protect
expect(ProtectedTag.count).to eq(1)
expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id])
@@ -27,19 +17,12 @@ RSpec.shared_examples "protected tags > access control > CE" do
visit project_protected_tags_path(project)
set_protected_tag_name('master')
-
- click_on "Protect"
+ set_allowed_to('create', 'No one')
+ click_on_protect
expect(ProtectedTag.count).to eq(1)
- within(".protected-tags-list") do
- find(".js-allowed-to-create").click
-
- within('.js-allowed-to-create-container') do
- expect(first("li")).to have_content("Roles")
- click_on access_type_name
- end
- end
+ set_allowed_to('create', access_type_name, form: '.protected-tags-list')
wait_for_requests
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index ff0b5bebe33..6e377b8cb0d 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -19,6 +19,13 @@ 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')
+ # Makes diffs show entire non-truncated values.
+ config.before(:each, :unlimited_max_formatted_output_length) do
+ config.expect_with :rspec do |c|
+ c.max_formatted_output_length = nil
+ end
+ end
+
unless ENV['CI']
# Allow running `:focus` examples locally,
# falling back to all tests when there is no `:focus` example.
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 7aa7d8e8abd..74b80c4864c 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -226,7 +226,6 @@
- './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'
@@ -957,7 +956,6 @@
- './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'
@@ -1577,7 +1575,6 @@
- './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'
@@ -3201,7 +3198,6 @@
- './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/geo_queue_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'
@@ -5084,7 +5080,6 @@
- './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'
@@ -5142,7 +5137,6 @@
- './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'
@@ -7956,7 +7950,6 @@
- './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'
@@ -8956,7 +8949,6 @@
- './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'
@@ -10381,18 +10373,14 @@
- './spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb'
- './spec/workers/concerns/application_worker_spec.rb'
- './spec/workers/concerns/cluster_agent_queue_spec.rb'
-- './spec/workers/concerns/cluster_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/queue_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/pipeline_background_queue_spec.rb'
-- './spec/workers/concerns/pipeline_queue_spec.rb'
- './spec/workers/concerns/project_import_options_spec.rb'
- './spec/workers/concerns/reenqueuer_spec.rb'
- './spec/workers/concerns/repository_check_queue_spec.rb'
diff --git a/spec/support/services/import_csv_service_shared_examples.rb b/spec/support/services/import_csv_service_shared_examples.rb
new file mode 100644
index 00000000000..1555497ae48
--- /dev/null
+++ b/spec/support/services/import_csv_service_shared_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples_for 'importer with email notification' do
+ it 'notifies user of import result' do
+ expect(Notify).to receive_message_chain(email_method, :deliver_later)
+
+ subject
+ end
+end
+
+RSpec.shared_examples 'correctly handles invalid files' do
+ shared_examples_for 'invalid file' do
+ it 'returns invalid file error' do
+ expect(subject[:success]).to eq(0)
+ expect(subject[:parse_error]).to eq(true)
+ end
+ end
+
+ context 'when given file with unsupported extension' do
+ let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
+
+ it_behaves_like 'invalid file'
+ end
+
+ context 'when given empty file' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') }
+
+ it_behaves_like 'invalid file'
+ end
+
+ context 'when given file without headers' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
+
+ it_behaves_like 'invalid file'
+ end
+end
diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb
index 0dea6cfb729..71740ba8ab2 100644
--- a/spec/support/services/issuable_import_csv_service_shared_examples.rb
+++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb
@@ -18,45 +18,14 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type|
end
end
- shared_examples_for 'importer with email notification' do
- it 'notifies user of import result' do
- expect(Notify).to receive_message_chain(email_method, :deliver_later)
-
- subject
- end
- end
-
- shared_examples_for 'invalid file' do
- it 'returns invalid file error' do
- expect(subject[:success]).to eq(0)
- expect(subject[:parse_error]).to eq(true)
- end
-
- it_behaves_like 'importer with email notification'
- it_behaves_like 'an issuable importer'
- end
-
describe '#execute' do
before do
project.add_developer(user)
end
- context 'invalid file extension' do
- let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
-
- it_behaves_like 'invalid file'
- end
-
- context 'empty file' do
- let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') }
-
- it_behaves_like 'invalid file'
- end
-
- context 'file without headers' do
- let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
-
- it_behaves_like 'invalid file'
+ it_behaves_like 'correctly handles invalid files' do
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
end
context 'with a file generated by Gitlab CSV export' do
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index 2c92ef64815..9612b657093 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -74,6 +74,12 @@ Integration.available_integration_names.each do |integration|
hash.merge!(k => File.read('spec/fixtures/ssl_key.pem'))
elsif integration == 'apple_app_store' && k == :app_store_key_id
hash.merge!(k => 'ABC1')
+ elsif integration == 'apple_app_store' && k == :app_store_private_key_file_name
+ hash.merge!(k => 'ssl_key.pem')
+ elsif integration == 'google_play' && k == :service_account_key
+ hash.merge!(k => File.read('spec/fixtures/service_account.json'))
+ elsif integration == 'google_play' && k == :service_account_key_file_name
+ hash.merge!(k => 'service_account.json')
else
hash.merge!(k => "someword")
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index b74819d2ac7..866a97b0e3c 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -5,10 +5,10 @@ RSpec.shared_context 'project navbar structure' do
let(:security_and_compliance_nav_item) do
{
- nav_item: _('Security & Compliance'),
+ nav_item: _('Security and Compliance'),
nav_sub_items: [
(_('Audit events') if Gitlab.ee?),
- _('Configuration')
+ _('Security configuration')
]
}
end
@@ -34,10 +34,10 @@ RSpec.shared_context 'project navbar structure' do
_('Commits'),
_('Branches'),
_('Tags'),
- _('Contributors'),
+ _('Contributor statistics'),
_('Graph'),
- _('Compare'),
- (_('Locked Files') if Gitlab.ee?)
+ _('Compare revisions'),
+ (_('Locked files') if Gitlab.ee?)
]
},
{
@@ -85,8 +85,7 @@ RSpec.shared_context 'project navbar structure' do
_('Metrics'),
_('Error Tracking'),
_('Alerts'),
- _('Incidents'),
- _('Airflow')
+ _('Incidents')
]
},
{
@@ -141,6 +140,7 @@ RSpec.shared_context 'group navbar structure' do
_('CI/CD'),
_('Applications'),
_('Packages and registries'),
+ s_('UsageQuota|Usage Quotas'),
_('Domain Verification')
]
}
@@ -155,18 +155,9 @@ RSpec.shared_context 'group navbar structure' do
}
end
- let(:administration_nav_item) do
- {
- nav_item: _('Administration'),
- nav_sub_items: [
- s_('UsageQuota|Usage Quotas')
- ]
- }
- end
-
let(:security_and_compliance_nav_item) do
{
- nav_item: _('Security & Compliance'),
+ nav_item: _('Security and Compliance'),
nav_sub_items: [
_('Audit events')
]
@@ -268,3 +259,30 @@ RSpec.shared_context 'dashboard navbar structure' do
]
end
end
+
+RSpec.shared_context '"Explore" navbar structure' do
+ let(:structure) do
+ [
+ {
+ nav_item: "Explore",
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Projects"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Groups"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Topics"),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _("Snippets"),
+ nav_sub_items: []
+ }
+ ]
+ end
+end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index 4c081c8464e..a9b12d47393 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -42,9 +42,7 @@ RSpec.shared_context 'GroupPolicy context' do
let(:developer_permissions) do
%i[
- create_metrics_dashboard_annotation
- delete_metrics_dashboard_annotation
- update_metrics_dashboard_annotation
+ admin_metrics_dashboard_annotation
create_custom_emoji
create_package
read_cluster
@@ -59,6 +57,7 @@ RSpec.shared_context 'GroupPolicy context' do
create_cluster update_cluster admin_cluster add_cluster
destroy_upload
admin_achievement
+ award_achievement
]
end
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 afc7fc8766f..5014a810f35 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -38,8 +38,8 @@ RSpec.shared_context 'ProjectPolicy context' do
read_commit_status read_confidential_issues read_container_image
read_harbor_registry read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus
- read_sentry_issue update_issue create_merge_request_in read_external_emails
- read_internal_note
+ read_sentry_issue update_issue create_merge_request_in
+ read_external_emails read_internal_note export_work_items
]
end
@@ -52,12 +52,11 @@ RSpec.shared_context 'ProjectPolicy context' do
admin_merge_request admin_tag create_build
create_commit_status create_container_image create_deployment
create_environment create_merge_request_from
- create_metrics_dashboard_annotation create_pipeline create_release
- create_wiki delete_metrics_dashboard_annotation
- destroy_container_image push_code read_pod_logs read_terraform_state
- resolve_note update_build update_commit_status update_container_image
- update_deployment update_environment update_merge_request
- update_metrics_dashboard_annotation update_pipeline update_release destroy_release
+ admin_metrics_dashboard_annotation create_pipeline create_release
+ create_wiki destroy_container_image push_code read_pod_logs
+ read_terraform_state resolve_note update_build update_commit_status
+ update_container_image update_deployment update_environment
+ update_merge_request update_pipeline update_release destroy_release
read_resource_group update_resource_group update_escalation_status
]
end
diff --git a/spec/support/shared_contexts/rack_attack_shared_context.rb b/spec/support/shared_contexts/rack_attack_shared_context.rb
index 12625ead72b..4e8c6e9dec6 100644
--- a/spec/support/shared_contexts/rack_attack_shared_context.rb
+++ b/spec/support/shared_contexts/rack_attack_shared_context.rb
@@ -5,8 +5,7 @@ RSpec.shared_context 'rack attack cache store' do
# Instead of test environment's :null_store so the throttles can increment
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
- # Make time-dependent tests deterministic
- freeze_time { example.run }
+ example.run
Rack::Attack.cache.store = Rails.cache
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 57967fb9414..ad64e4d5be5 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
@@ -58,6 +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_sources) { { private: private_component_file_sources, public: public_component_file_sources }[visibility_level] }
+ let(:component_file_di) { { private: private_component_file_di, public: public_component_file_di }[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] }
diff --git a/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb b/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb
index dc5195e4b01..1fa1a5c69f4 100644
--- a/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb
+++ b/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_context '"Security & Compliance" permissions' do
+RSpec.shared_context '"Security and Compliance" permissions' do
let(:project_instance) { an_instance_of(Project) }
let(:user_instance) { an_instance_of(User) }
let(:before_request_defined) { false }
@@ -18,7 +18,7 @@ RSpec.shared_context '"Security & Compliance" permissions' do
allow(Ability).to receive(:allowed?).with(user_instance, :access_security_and_compliance, project_instance).and_return(true)
end
- context 'when the "Security & Compliance" feature is disabled' do
+ context 'when the "Security and Compliance" feature is disabled' do
subject { response }
before do
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
new file mode 100644
index 00000000000..b324a5886a9
--- /dev/null
+++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
@@ -0,0 +1,464 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'value stream analytics flow metrics issueCount examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ let_it_be(:issue1) { create(:issue, project: project1, author: author, created_at: 12.days.ago) }
+ let_it_be(:issue2) { create(:issue, project: project2, author: author, created_at: 13.days.ago) }
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ created_at: 14.days.ago)
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ created_at: 15.days.ago)
+ end
+
+ let_it_be(:issue_outside_of_the_range) { create(:issue, project: project2, author: author, created_at: 50.days.ago) }
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ issueCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'issueCount')
+ end
+
+ it 'returns the correct count' do
+ expect(result).to eq({
+ 'identifier' => 'issues',
+ 'unit' => nil,
+ 'value' => 4,
+ 'title' => n_('New Issue', 'New Issues', 4)
+ })
+ end
+
+ context 'with partial filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to eq({
+ 'identifier' => 'issues',
+ 'unit' => nil,
+ 'value' => 2,
+ 'title' => n_('New Issue', 'New Issues', 2)
+ })
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to eq({
+ 'identifier' => 'issues',
+ 'unit' => nil,
+ 'value' => 1,
+ 'title' => n_('New Issue', 'New Issues', 1)
+ })
+ end
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examples' do
+ let_it_be(:deployment1) do
+ create(:deployment, :success, environment: production_environment1, finished_at: 5.days.ago)
+ end
+
+ let_it_be(:deployment2) do
+ create(:deployment, :success, environment: production_environment2, finished_at: 10.days.ago)
+ end
+
+ let_it_be(:deployment3) do
+ create(:deployment, :success, environment: production_environment2, finished_at: 15.days.ago)
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 12.days.ago.iso8601,
+ to: 3.days.ago.iso8601
+ }
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ deploymentCount(from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'deploymentCount')
+ end
+
+ it 'returns the correct count' do
+ expect(result).to eq({
+ 'identifier' => 'deploys',
+ 'unit' => nil,
+ 'value' => 2,
+ 'title' => n_('Deploy', 'Deploys', 2)
+ })
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 20.days.ago.iso8601,
+ to: 18.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to eq({
+ 'identifier' => 'deploys',
+ 'unit' => nil,
+ 'value' => 0,
+ 'title' => n_('Deploy', 'Deploys', 0)
+ })
+ end
+ end
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics leadTime examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ let_it_be(:issue1) do
+ create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago)
+ end
+
+ let_it_be(:issue2) do
+ create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago)
+ end
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ created_at: 14.days.ago,
+ closed_at: 11.days.ago)
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ created_at: 20.days.ago,
+ closed_at: 15.days.ago)
+ end
+
+ before do
+ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ leadTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ links {
+ label
+ url
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 21.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'leadTime')
+ end
+
+ it 'returns the correct value' do
+ expect(result).to match(a_hash_including({
+ 'identifier' => 'lead_time',
+ 'unit' => n_('day', 'days', 4),
+ 'value' => 4,
+ 'title' => _('Lead Time'),
+ 'links' => [
+ { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) },
+ { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) }
+ ]
+ }))
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 30.days.ago.iso8601,
+ to: 25.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to match(a_hash_including({ 'value' => nil }))
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to match(a_hash_including({ 'value' => 3 }))
+ end
+ end
+end
+
+RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' do
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:label) { create(:group_label, group: group) }
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+
+ let_it_be(:issue1) do
+ create(:issue, project: project1, author: author, closed_at: 12.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 17.days.ago)
+ end
+ end
+
+ let_it_be(:issue2) do
+ create(:issue, project: project2, author: author, closed_at: 13.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 16.days.ago)
+ end
+ end
+
+ let_it_be(:issue3) do
+ create(:labeled_issue,
+ project: project1,
+ labels: [label],
+ author: author,
+ milestone: milestone,
+ assignees: [assignee],
+ closed_at: 11.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 14.days.ago)
+ end
+ end
+
+ let_it_be(:issue4) do
+ create(:labeled_issue,
+ project: project2,
+ labels: [label],
+ assignees: [assignee],
+ closed_at: 15.days.ago).tap do |issue|
+ issue.metrics.update!(first_mentioned_in_commit_at: 20.days.ago)
+ end
+ end
+
+ before do
+ Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
+ end
+
+ let(:query) do
+ <<~QUERY
+ query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
+ #{context}(fullPath: $path) {
+ flowMetrics {
+ cycleTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
+ value
+ unit
+ identifier
+ title
+ links {
+ label
+ url
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ let(:variables) do
+ {
+ path: full_path,
+ from: 21.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ subject(:result) do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ graphql_data.dig(context.to_s, 'flowMetrics', 'cycleTime')
+ end
+
+ it 'returns the correct value' do
+ expect(result).to eq({
+ 'identifier' => 'cycle_time',
+ 'unit' => n_('day', 'days', 4),
+ 'value' => 4,
+ 'title' => _('Cycle Time'),
+ 'links' => []
+ })
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ expect(result).to eq(nil)
+ end
+ end
+
+ context 'when outside of the date range' do
+ let(:variables) do
+ {
+ path: full_path,
+ from: 30.days.ago.iso8601,
+ to: 25.days.ago.iso8601
+ }
+ end
+
+ it 'returns 0 count' do
+ expect(result).to match(a_hash_including({ 'value' => nil }))
+ end
+ end
+
+ context 'with all filters' do
+ let(:variables) do
+ {
+ path: full_path,
+ assigneeUsernames: [assignee.username],
+ labelNames: [label.title],
+ authorUsername: author.username,
+ milestoneTitle: milestone.title,
+ from: 20.days.ago.iso8601,
+ to: 10.days.ago.iso8601
+ }
+ end
+
+ it 'returns filtered count' do
+ expect(result).to match(a_hash_including({ 'value' => 3 }))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
new file mode 100644
index 00000000000..f1ffddf6507
--- /dev/null
+++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'unlicensed cycle analytics request params' do
+ let(:params) do
+ {
+ created_after: '2019-01-01',
+ created_before: '2019-03-01',
+ project_ids: [2, 3],
+ namespace: namespace,
+ current_user: user
+ }
+ end
+
+ subject { described_class.new(params) }
+
+ before do
+ root_group.add_owner(user)
+ end
+
+ describe 'validations' do
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+
+ context 'when `created_before` is missing' do
+ before do
+ params[:created_before] = nil
+ end
+
+ it 'is valid', time_travel_to: '2019-03-01' do
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'when `created_before` is earlier than `created_after`' do
+ before do
+ params[:created_before] = '2015-01-01'
+ end
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:created_before]).not_to be_empty
+ end
+ end
+
+ context 'when the date range exceeds 180 days' do
+ before do
+ params[:created_before] = '2019-07-15'
+ end
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ message = s_('CycleAnalytics|The given date range is larger than 180 days')
+ expect(subject.errors.messages[:created_after]).to include(message)
+ end
+ end
+ end
+
+ it 'casts `created_after` to `Time`' do
+ expect(subject.created_after).to be_a_kind_of(Time)
+ end
+
+ it 'casts `created_before` to `Time`' do
+ expect(subject.created_before).to be_a_kind_of(Time)
+ end
+
+ describe 'optional `value_stream`' do
+ context 'when `value_stream` is not empty' do
+ let(:value_stream) { instance_double('Analytics::CycleAnalytics::ValueStream') }
+
+ before do
+ params[:value_stream] = value_stream
+ end
+
+ it { expect(subject.value_stream).to eq(value_stream) }
+ end
+
+ context 'when `value_stream` is nil' do
+ before do
+ params[:value_stream] = nil
+ end
+
+ it { expect(subject.value_stream).to eq(nil) }
+ end
+ end
+
+ describe 'sorting params' do
+ before do
+ params.merge!(sort: 'duration', direction: 'asc')
+ end
+
+ it 'converts sorting params to symbol when passing it to data collector' do
+ data_collector_params = subject.to_data_collector_params
+
+ expect(data_collector_params[:sort]).to eq(:duration)
+ expect(data_collector_params[:direction]).to eq(:asc)
+ end
+
+ it 'adds sorting params to data attributes' do
+ data_attributes = subject.to_data_attributes
+
+ expect(data_attributes[:sort]).to eq('duration')
+ expect(data_attributes[:direction]).to eq('asc')
+ end
+ end
+
+ describe 'aggregation params' do
+ context 'when not licensed' do
+ it 'returns nil' do
+ data_collector_params = subject.to_data_attributes
+ expect(data_collector_params[:aggregation]).to eq(nil)
+ end
+ end
+ end
+
+ describe 'use_aggregated_data_collector param' do
+ subject(:value) { described_class.new(params).to_data_collector_params[:use_aggregated_data_collector] }
+
+ it { is_expected.to eq(false) }
+ end
+
+ describe 'enable_tasks_by_type_chart data attribute' do
+ subject(:value) { described_class.new(params).to_data_attributes[:enable_tasks_by_type_chart] }
+
+ it { is_expected.to eq('false') }
+ 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 7df4b7635d3..ddd3bbd636a 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
@@ -80,7 +80,7 @@ RSpec.shared_examples 'multiple issue boards' do
click_button 'Select a label'
- page.choose(planning.title)
+ find('label', text: planning.title).click
click_button 'Add to board'
diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
index cc28a79b4ca..e75188f8249 100644
--- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
@@ -60,19 +60,6 @@ RSpec.shared_examples Repositories::GitHttpController do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'updates the user activity' do
- activity_project = container.is_a?(PersonalSnippet) ? nil : project
-
- activity_service = instance_double(Users::ActivityService)
-
- args = { author: user, project: activity_project, namespace: activity_project&.namespace }
- expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service)
-
- expect(activity_service).to receive(:execute)
-
- get :info_refs, params: params
- end
-
include_context 'parsed logs' do
it 'adds user info to the logs' do
get :info_refs, params: params
@@ -87,14 +74,20 @@ RSpec.shared_examples Repositories::GitHttpController do
end
describe 'POST #git_upload_pack' do
- before do
+ it 'returns 200' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
- end
- it 'returns 200' do
post :git_upload_pack, params: params
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when JWT token is not provided' do
+ it 'returns 403' do
+ post :git_upload_pack, params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index 44f30c32472..b6339607d6b 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -6,8 +6,6 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
def register_device(device_type, **kwargs)
case device_type.downcase
- when "u2f"
- register_u2f_device(**kwargs)
when "webauthn"
register_webauthn_device(**kwargs)
else
diff --git a/spec/support/shared_examples/features/abuse_report_shared_examples.rb b/spec/support/shared_examples/features/abuse_report_shared_examples.rb
new file mode 100644
index 00000000000..7a520fb0cd2
--- /dev/null
+++ b/spec/support/shared_examples/features/abuse_report_shared_examples.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'reports the user with an abuse category' do
+ it 'creates abuse report' do
+ click_button 'Report abuse to administrator'
+ choose "They're posting spam."
+ click_button 'Next'
+
+ fill_in 'abuse_report_message', with: 'This user sends spam'
+ click_button 'Send report'
+
+ expect(page).to have_content 'Thank you for your report'
+ end
+end
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 6cd9c4ce1c4..7582e67efbd 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -1,44 +1,9 @@
# frozen_string_literal: true
RSpec.shared_examples 'edits content using the content editor' do
- let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
-
- def switch_to_content_editor
- click_button _('View rich text')
- click_button _('Rich text')
- 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
+ include ContentEditorHelpers
- def upload_asset(fixture_name)
- attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true)
- end
-
- def wait_until_hidden_field_is_updated(value)
- expect(page).to have_field('wiki[content]', with: value, type: 'hidden')
- end
+ let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
it 'saves page content in local storage if the user navigates away' do
switch_to_content_editor
@@ -52,16 +17,6 @@ RSpec.shared_examples 'edits content using the content editor' do
refresh
expect(page).to have_text('Typing text in the content editor')
-
- refresh # also retained after second refresh
-
- expect(page).to have_text('Typing text in the content editor')
-
- click_link 'Cancel' # draft is deleted on cancel
-
- page.go_back
-
- expect(page).not_to have_text('Typing text in the content editor')
end
describe 'formatting bubble menu' do
@@ -92,25 +47,18 @@ RSpec.shared_examples 'edits content using the content editor' do
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
+ it 'displays correct media bubble menu for images', :js do
+ display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png'
expect_formatting_menu_to_be_hidden
expect_media_bubble_menu_to_be_visible
end
- 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
-
it 'displays correct media bubble menu for video', :js do
- test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
+ display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4'
+
+ expect_formatting_menu_to_be_hidden
+ expect_media_bubble_menu_to_be_visible
end
end
@@ -225,7 +173,7 @@ RSpec.shared_examples 'edits content using the content editor' do
before do
if defined?(project)
create(:issue, project: project, title: 'My Cool Linked Issue')
- create(:merge_request, source_project: project, title: 'My Cool Merge Request')
+ create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:label, project: project, title: 'My Cool Label')
create(:milestone, project: project, title: 'My Cool Milestone')
@@ -234,7 +182,7 @@ RSpec.shared_examples 'edits content using the content editor' do
project = create(:project, group: group)
create(:issue, project: project, title: 'My Cool Linked Issue')
- create(:merge_request, source_project: project, title: 'My Cool Merge Request')
+ create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
create(:group_label, group: group, title: 'My Cool Label')
create(:milestone, group: group, title: 'My Cool Milestone')
@@ -251,7 +199,9 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(find(suggestions_dropdown)).to have_text('abc123')
expect(find(suggestions_dropdown)).to have_text('all')
- expect(find(suggestions_dropdown)).to have_text('Group Members (2)')
+ expect(find(suggestions_dropdown)).to have_text('Group Members')
+
+ type_in_content_editor 'bc'
send_keys [:arrow_down, :enter]
@@ -332,3 +282,24 @@ RSpec.shared_examples 'edits content using the content editor' do
end
end
end
+
+RSpec.shared_examples 'inserts diagrams.net diagram using the content editor' do
+ include ContentEditorHelpers
+
+ before do
+ switch_to_content_editor
+
+ open_insert_media_dropdown
+ end
+
+ it 'displays correct media bubble menu with edit diagram button' do
+ display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg'
+
+ expect_formatting_menu_to_be_hidden
+ expect_media_bubble_menu_to_be_visible
+
+ click_edit_diagram_button
+
+ expect_drawio_editor_is_opened
+ end
+end
diff --git a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
index dab125caa60..b8e42843e6f 100644
--- a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
+++ b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'for each incident details route' do |example, tab_text:|
+RSpec.shared_examples 'for each incident details route' do |example, tab_text:, tab:|
before do
sign_in(user)
visit incident_path
@@ -25,4 +25,16 @@ RSpec.shared_examples 'for each incident details route' do |example, tab_text:|
it_behaves_like example
end
+
+ context "for /-/issues/incident/:id/#{tab} route" do
+ let(:incident_path) { incident_project_issues_path(project, incident, tab) }
+
+ it_behaves_like example
+ end
+
+ context "for /-/issues/:id/#{tab} route" do
+ let(:incident_path) { incident_issue_project_issue_path(project, incident, tab) }
+
+ it_behaves_like example
+ end
end
diff --git a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
index 4c312b42c0a..148ff2cfb54 100644
--- a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
+++ b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'user activates the Mattermost Slash Command integration'
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
- expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ expect(token_placeholder).to eq('')
end
it 'redirects to the integrations page after saving but not activating' do
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index b6f7094e422..13adcfe9191 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -17,8 +17,6 @@ RSpec.shared_examples 'issuable invite members' do
page.within '.dropdown-menu-user' do
expect(page).to have_link('Invite Members')
- expect(page).to have_selector('[data-track-action="click_invite_members"]')
- expect(page).to have_selector('[data-track-label="edit_assignee"]')
end
click_link 'Invite Members'
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 b59f3f1e27b..63ba5832771 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -5,87 +5,40 @@ RSpec.shared_examples 'manage applications' do
let_it_be(:application_name_changed) { "#{application_name} changed" }
let_it_be(:application_redirect_uri) { 'https://foo.bar' }
- 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
+ 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_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)
+ 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)
- expect(page).to have_css("button[title=\"Copy 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'
-
- 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'
+ 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')
+ expect(page).to have_content _('The secret is only available when you create the application or renew the secret.')
- visit_applications_path
+ visit_applications_path
- page.within '.oauth-applications' do
- click_on 'Destroy'
- end
- expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
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/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
new file mode 100644
index 00000000000..cc0984b6226
--- /dev/null
+++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Deploy keys with protected tags' do
+ let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] }
+
+ context 'when deploy keys are enabled to this project' do
+ let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) }
+ let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) }
+
+ context 'when only one deploy key can push' do
+ before do
+ deploy_key_1.deploy_keys_projects.first.update!(can_push: true)
+ end
+
+ it "shows all dropdown sections in the 'Allowed to create' main dropdown, with only one deploy key" do
+ visit project_protected_tags_path(project)
+
+ find(".js-allowed-to-create").click
+ wait_for_requests
+
+ within('[data-testid="allowed-to-create-dropdown"]') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
+ expect(page).to have_content('title 1')
+ expect(page).not_to have_content('title 2')
+ end
+ end
+
+ it "shows all sections in the 'Allowed to create' update dropdown" do
+ create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0')
+
+ visit project_protected_tags_path(project)
+
+ within(".js-protected-tag-edit-form") do
+ find(".js-allowed-to-create").click
+ wait_for_requests
+
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
+ end
+ end
+ end
+
+ context 'when no deploy key can push' do
+ it "just shows all sections but not deploy keys in the 'Allowed to create' dropdown" do
+ visit project_protected_tags_path(project)
+
+ find(".js-allowed-to-create").click
+ wait_for_requests
+
+ within('[data-testid="allowed-to-create-dropdown"]') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb
index 028e075c87a..231406289b4 100644
--- a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb
+++ b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'Secure OAuth Authorizations' do
end
context 'when user is unconfirmed' do
- let(:user) { create(:user, confirmed_at: nil) }
+ let(:user) { create(:user, :unconfirmed) }
it 'displays an error' do
expect(page).to have_text I18n.t('doorkeeper.errors.messages.unconfirmed_email')
diff --git a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb
index 8304a91af86..81c9ac1164b 100644
--- a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb
+++ b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb
@@ -1,59 +1,38 @@
# frozen_string_literal: true
RSpec.shared_examples 'user email validation' do
- let(:email_hint_message) { 'We recommend a work email address.' }
- let(:email_error_message) { 'Please provide a valid email address.' }
+ let(:email_hint_message) { _('We recommend a work email address.') }
+ let(:email_error_message) { _('Please provide a valid email address.') }
let(:email_warning_message) do
- 'This email address does not look right, are you sure you typed it correctly?'
+ _('This email address does not look right, are you sure you typed it correctly?')
end
- context 'with trial_email_validation flag enabled' do
- it 'shows an error message until a correct email is entered' do
- visit path
- expect(page).to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
+ it 'shows an error message until a correct email is entered' do
+ visit path
+ expect(page).to have_content(email_hint_message)
+ expect(page).not_to have_content(email_error_message)
+ expect(page).not_to have_content(email_warning_message)
- fill_in 'new_user_email', with: 'foo@'
- fill_in 'new_user_first_name', with: ''
+ fill_in 'new_user_email', with: 'foo@'
+ fill_in 'new_user_first_name', with: ''
- expect(page).not_to have_content(email_hint_message)
- expect(page).to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
+ expect(page).not_to have_content(email_hint_message)
+ expect(page).to have_content(email_error_message)
+ expect(page).not_to have_content(email_warning_message)
- fill_in 'new_user_email', with: 'foo@bar'
- fill_in 'new_user_first_name', with: ''
+ fill_in 'new_user_email', with: 'foo@bar'
+ fill_in 'new_user_first_name', with: ''
- expect(page).not_to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).to have_content(email_warning_message)
+ expect(page).not_to have_content(email_hint_message)
+ expect(page).not_to have_content(email_error_message)
+ expect(page).to have_content(email_warning_message)
- fill_in 'new_user_email', with: 'foo@gitlab.com'
- fill_in 'new_user_first_name', with: ''
+ fill_in 'new_user_email', with: 'foo@gitlab.com'
+ fill_in 'new_user_first_name', with: ''
- expect(page).not_to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
- end
- end
-
- context 'when trial_email_validation flag disabled' do
- before do
- stub_feature_flags trial_email_validation: false
- end
-
- it 'does not show an error message' do
- visit path
- expect(page).to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
-
- fill_in 'new_user_email', with: 'foo@'
-
- expect(page).to have_content(email_hint_message)
- expect(page).not_to have_content(email_error_message)
- expect(page).not_to have_content(email_warning_message)
- end
+ expect(page).not_to have_content(email_hint_message)
+ expect(page).not_to have_content(email_error_message)
+ expect(page).not_to have_content(email_warning_message)
end
end
diff --git a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb
new file mode 100644
index 00000000000..0b0c9edcb42
--- /dev/null
+++ b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'variable list pagination' do |variable_type|
+ first_page_count = 20
+
+ before do
+ first_page_count.times do |i|
+ case variable_type
+ when :ci_variable
+ create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, project: project)
+ when :ci_group_variable
+ create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, group: group)
+ else
+ create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true)
+ end
+ end
+
+ visit page_path
+ wait_for_requests
+ end
+
+ it 'can navigate between pages' do
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(page.all('.js-ci-variable-row').length).to be(first_page_count)
+ end
+
+ click_button 'Next'
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(page.all('.js-ci-variable-row').length).to be(1)
+ end
+
+ click_button 'Previous'
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(page.all('.js-ci-variable-row').length).to be(first_page_count)
+ end
+ end
+
+ it 'sorts variables alphabetically in ASC and DESC order' do
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
+ expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_8')
+ end
+
+ click_button 'Next'
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9')
+ end
+
+ page.within('[data-testid="ci-variable-table"]') do
+ find('.b-table-sort-icon-left').click
+ end
+
+ wait_for_requests
+
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9')
+ expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_0')
+ end
+ 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 0334187e4b1..c1e4185e058 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
@@ -150,6 +150,7 @@ RSpec.shared_examples 'User updates wiki page' do
end
it_behaves_like 'edits content using the content editor'
+ it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
index 639eb3f2b99..b3378c76658 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
@@ -84,6 +84,30 @@ RSpec.shared_examples 'User views wiki sidebar' do
expect(page).not_to have_link('View All Pages')
end
+ it 'shows all collapse buttons in the sidebar' do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3)
+ end
+ end
+
+ it 'collapses/expands children when click collapse/expand button in the sidebar', :js do
+ visit wiki_path(wiki)
+
+ within('.right-sidebar') do
+ first("[data-testid='chevron-down-icon']").click
+ (11..15).each { |i| expect(page).not_to have_content("my page #{i}") }
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(1)
+ expect(page.all("[data-testid='chevron-right-icon']").size).to eq(1)
+
+ first("[data-testid='chevron-right-icon']").click
+ (11..15).each { |i| expect(page).to have_content("my page #{i}") }
+ expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3)
+ expect(page.all("[data-testid='chevron-right-icon']").size).to eq(0)
+ end
+ end
+
context 'when there are more than 15 existing pages' do
before do
create(:wiki_page, wiki: wiki, title: 'my page 16')
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 4f3d957ad71..0b8bfc4d2a2 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -1,5 +1,20 @@
# frozen_string_literal: true
+RSpec.shared_examples 'work items title' do
+ let(:title_selector) { '[data-testid="work-item-title"]' }
+
+ it 'successfully shows and changes the title of the work item' do
+ expect(work_item.reload.title).to eq work_item.title
+
+ find(title_selector).set("Work item title")
+ find(title_selector).native.send_keys(:return)
+
+ wait_for_requests
+
+ expect(work_item.reload.title).to eq 'Work item title'
+ end
+end
+
RSpec.shared_examples 'work items status' do
let(:state_selector) { '[data-testid="work-item-state-select"]' }
@@ -19,7 +34,7 @@ RSpec.shared_examples 'work items comments' do
let(:form_selector) { '[data-testid="work-item-add-comment"]' }
it 'successfully creates and shows comments' do
- click_button 'Add a comment'
+ click_button 'Add a reply'
find(form_selector).fill_in(with: "Test comment")
click_button "Comment"
@@ -39,7 +54,6 @@ RSpec.shared_examples 'work items assignees' do
# submit and simulate blur to save
send_keys(:enter)
find("body").click
-
wait_for_requests
expect(work_item.assignees).to include(user)
@@ -47,6 +61,8 @@ RSpec.shared_examples 'work items assignees' do
end
RSpec.shared_examples 'work items labels' do
+ let(:label_title_selector) { '[data-testid="labels-title"]' }
+
it 'successfully assigns a label' do
label = create(:label, project: work_item.project, title: "testing-label")
@@ -55,8 +71,7 @@ RSpec.shared_examples 'work items labels' do
# submit and simulate blur to save
send_keys(:enter)
- find("body").click
-
+ find(label_title_selector).click
wait_for_requests
expect(work_item.labels).to include(label)
@@ -139,3 +154,27 @@ RSpec.shared_examples 'work items invite members' do
end
end
end
+
+RSpec.shared_examples 'work items milestone' do
+ def set_milestone(milestone_dropdown, milestone_text)
+ milestone_dropdown.click
+
+ find('[data-testid="work-item-milestone-dropdown"] .gl-form-input', visible: true).send_keys "\"#{milestone_text}\""
+ wait_for_requests
+
+ click_button(milestone_text)
+ wait_for_requests
+ end
+
+ let(:milestone_dropdown_selector) { '[data-testid="work-item-milestone-dropdown"]' }
+
+ it 'searches and sets or removes milestone for the work item' do
+ set_milestone(find(milestone_dropdown_selector), milestone.title)
+
+ expect(page.find(milestone_dropdown_selector)).to have_text(milestone.title)
+
+ set_milestone(find(milestone_dropdown_selector), 'No milestone')
+
+ expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone')
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb
new file mode 100644
index 00000000000..e885b5d283e
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'members bulk update mutation' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:member1) { create(member_type, source: source, user: user1) }
+ let_it_be(:member2) { create(member_type, source: source, user: user2) }
+
+ let(:extra_params) { { expires_at: 10.days.from_now } }
+ let(:input_params) { input.merge(extra_params) }
+ let(:mutation) { graphql_mutation(mutation_name, input_params) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name) }
+
+ let(:input) do
+ {
+ source_id_key => source.to_global_id.to_s,
+ 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
+ 'access_level' => 'GUEST'
+ }
+ end
+
+ context 'when user is not logged-in' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user is not an owner' do
+ before do
+ source.add_developer(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user is an owner' do
+ before do
+ source.add_owner(current_user)
+ end
+
+ shared_examples 'updates the user access role' do
+ specify do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ new_access_levels = mutation_response[response_member_field].map do |member|
+ member['accessLevel']['integerValue']
+ end
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(new_access_levels).to all(be Gitlab::Access::GUEST)
+ end
+ end
+
+ it_behaves_like 'updates the user access role'
+
+ context 'when inherited members are passed' do
+ let(:input) do
+ {
+ source_id_key => source.to_global_id.to_s,
+ 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, parent_group_member.user.to_global_id.to_s],
+ 'access_level' => 'GUEST'
+ }
+ end
+
+ it 'does not update the members' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ error = Mutations::Members::BulkUpdateBase::INVALID_MEMBERS_ERROR
+ expect(json_response['errors'].first['message']).to include(error)
+ end
+ end
+
+ context 'when members count is more than the allowed limit' do
+ let(:max_members_update_limit) { 1 }
+
+ before do
+ stub_const('Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
+ end
+
+ it 'does not update the members' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ error = Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_ERROR
+ expect(json_response['errors'].first['message']).to include(error)
+ end
+ end
+
+ context 'when the update service raises access denied error' do
+ before do
+ allow_next_instance_of(Members::UpdateService) do |instance|
+ allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ it 'does not update the members' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response[response_member_field]).to be_nil
+ expect(mutation_response['errors'])
+ .to contain_exactly("Unable to update members, please check user permissions.")
+ end
+ end
+
+ context 'when the update service returns an error message' do
+ before do
+ allow_next_instance_of(Members::UpdateService) do |instance|
+ error_result = {
+ message: 'Expires at cannot be a date in the past',
+ status: :error,
+ members: [member1]
+ }
+ allow(instance).to receive(:execute).and_return(error_result)
+ end
+ end
+
+ it 'will pass through the error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response[response_member_field].first['id']).to eq(member1.to_global_id.to_s)
+ expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
index 0d2e9f6ec8c..09239ced73a 100644
--- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
@@ -46,7 +46,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do
discussions {
edges {
node {
- #{all_graphql_fields_for('Discussion', max_depth: 4)}
+ #{all_graphql_fields_for('Discussion', max_depth: 4, excluded: ['productAnalyticsState'])}
}
}
}
diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
new file mode 100644
index 00000000000..5f4e7e5d4e7
--- /dev/null
+++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work item supports assignee widget updates via quick actions' do
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+
+ context 'when assigning a user' do
+ let(:body) { "/assign @#{developer.username}" }
+
+ it 'updates the work item assignee' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.assignee_ids }.from([]).to([developer.id])
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'when unassigning a user' do
+ let(:body) { "/unassign @#{developer.username}" }
+
+ before do
+ noteable.update!(assignee_ids: [developer.id])
+ end
+
+ it 'updates the work item assignee' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.assignee_ids }.from([developer.id]).to([])
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+end
+
+RSpec.shared_examples 'work item does not support assignee widget updates via quick actions' do
+ let(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+ let(:body) { "Updating assignee.\n/assign @#{developer.username}" }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.assignee_ids }
+ end
+end
+
+RSpec.shared_examples 'work item supports labels widget updates via quick actions' do
+ shared_examples 'work item labels are updated' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.labels.count }.to(expected_labels.count)
+
+ expect(noteable.labels).to match_array(expected_labels)
+ end
+ end
+
+ let_it_be(:existing_label) { create(:label, project: project) }
+ let_it_be(:label1) { create(:label, project: project) }
+ let_it_be(:label2) { create(:label, project: project) }
+
+ let(:add_label_ids) { [] }
+ let(:remove_label_ids) { [] }
+
+ before_all do
+ noteable.update!(labels: [existing_label])
+ end
+
+ context 'when only removing labels' do
+ let(:remove_label_ids) { [existing_label.to_gid.to_s] }
+ let(:expected_labels) { [] }
+ let(:body) { "/remove_label ~\"#{existing_label.name}\"" }
+
+ it_behaves_like 'work item labels are updated'
+ end
+
+ context 'when only adding labels' do
+ let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] }
+ let(:expected_labels) { [label1, label2, existing_label] }
+ let(:body) { "/labels ~\"#{label1.name}\" ~\"#{label2.name}\"" }
+
+ it_behaves_like 'work item labels are updated'
+ end
+
+ context 'when adding and removing labels' do
+ let(:remove_label_ids) { [existing_label.to_gid.to_s] }
+ let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] }
+ let(:expected_labels) { [label1, label2] }
+ let(:body) { "/label ~\"#{label1.name}\" ~\"#{label2.name}\"\n/remove_label ~\"#{existing_label.name}\"" }
+
+ it_behaves_like 'work item labels are updated'
+ end
+end
+
+RSpec.shared_examples 'work item does not support labels widget updates via quick actions' do
+ let(:label1) { create(:label, project: project) }
+ let(:body) { "Updating labels.\n/labels ~\"#{label1.name}\"" }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:labels).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.labels.count }
+
+ expect(noteable.labels).to be_empty
+ end
+end
+
+RSpec.shared_examples 'work item supports start and due date widget updates via quick actions' do
+ let(:due_date) { Date.today }
+ let(:body) { "/remove_due_date" }
+
+ before do
+ noteable.update!(due_date: due_date)
+ end
+
+ it 'updates start and due date' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to not_change(noteable, :start_date).and(
+ change { noteable.due_date }.from(due_date).to(nil)
+ )
+ end
+end
+
+RSpec.shared_examples 'work item does not support start and due date widget updates via quick actions' do
+ let(:body) { "Updating due date.\n/due today" }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:start_and_due_date).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.due_date }
+ end
+end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index bb33a7559dc..ed193d06161 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -42,6 +42,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
profileEnableGitpodPath
savedReplies
savedReply
+ user_achievements
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb
new file mode 100644
index 00000000000..b3d3000aa06
--- /dev/null
+++ b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'CalloutsHelper#web_hook_disabled_dismissed shared examples' do
+ context 'when the web-hook failure callout has never been dismissed' do
+ it 'is false' do
+ expect(helper).not_to be_web_hook_disabled_dismissed(container)
+ end
+ end
+
+ context 'when the web-hook failure callout has been dismissed', :freeze_time, :clean_gitlab_redis_shared_state do
+ before do
+ create(factory,
+ feature_name: Users::CalloutsHelper::WEB_HOOK_DISABLED,
+ user: user,
+ dismissed_at: 1.week.ago,
+ container_key => container)
+ end
+
+ it 'is true' do
+ expect(helper).to be_web_hook_disabled_dismissed(container)
+ end
+
+ it 'is true when passed as a presenter' do
+ skip "Does not apply to #{container.class}" unless container.is_a?(Presentable)
+
+ expect(helper).to be_web_hook_disabled_dismissed(container.present)
+ end
+
+ context 'when there was an older failure' do
+ before do
+ Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) }
+ end
+
+ it 'is true' do
+ expect(helper).to be_web_hook_disabled_dismissed(container)
+ end
+ end
+
+ context 'when there has been a more recent failure' do
+ before do
+ Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) }
+ end
+
+ it 'is false' do
+ expect(helper).not_to be_web_hook_disabled_dismissed(container)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
index aeb4e0feb12..a21eddb182c 100644
--- a/spec/support/shared_examples/integrations/integration_settings_form.rb
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -7,7 +7,6 @@ RSpec.shared_examples 'integration settings form' do
it 'displays all the integrations', feature_category: :integrations do
aggregate_failures do
integrations.each do |integration|
- stub_feature_flags(integration_slack_app_notifications: false)
navigate_to_integration(integration)
page.within('form.integration-settings-form') do
diff --git a/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb
new file mode 100644
index 00000000000..b88eade7db2
--- /dev/null
+++ b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'it depends on value of the `terraform_state.enabled` config' do |params = {}|
+ let(:expected_success_status) { params[:success_status] || :ok }
+
+ context 'when terraform_state.enabled=false' do
+ before do
+ stub_config(terraform_state: { enabled: false })
+ end
+
+ it 'returns `forbidden` response' do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when terraform_state.enabled=true' do
+ before do
+ stub_config(terraform_state: { enabled: true })
+ end
+
+ it 'returns a successful response' do
+ request
+
+ expect(response).to have_gitlab_http_status(expected_success_status)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb
new file mode 100644
index 00000000000..b9d71183851
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'async constraints validation' do
+ include ExclusiveLeaseHelpers
+
+ let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION }
+
+ let(:constraints_model) { Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation }
+ let(:table_name) { '_test_async_constraints' }
+ let(:constraint_name) { 'constraint_parent_id' }
+
+ let(:validation) do
+ create(:postgres_async_constraint_validation,
+ table_name: table_name,
+ name: constraint_name,
+ constraint_type: constraint_type)
+ end
+
+ let(:connection) { validation.connection }
+
+ subject { described_class.new(validation) }
+
+ it 'validates the constraint while controlling statement timeout' do
+ allow(connection).to receive(:execute).and_call_original
+ expect(connection).to receive(:execute)
+ .with("SET statement_timeout TO '43200s'").ordered.and_call_original
+ expect(connection).to receive(:execute)
+ .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/).ordered.and_call_original
+ expect(connection).to receive(:execute)
+ .with("RESET statement_timeout").ordered.and_call_original
+
+ subject.perform
+ end
+
+ it 'removes the constraint validation record from table' do
+ expect(validation).to receive(:destroy!).and_call_original
+
+ expect { subject.perform }.to change { constraints_model.count }.by(-1)
+ end
+
+ it 'skips logic if not able to acquire exclusive lease' do
+ expect(lease).to receive(:try_obtain).ordered.and_return(false)
+ expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
+ expect(validation).not_to receive(:destroy!)
+
+ expect { subject.perform }.not_to change { constraints_model.count }
+ end
+
+ it 'logs messages around execution' do
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Starting to validate constraint'))
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Finished validating constraint'))
+ end
+
+ context 'when the constraint does not exist' do
+ before do
+ connection.create_table(table_name, force: true)
+ end
+
+ it 'skips validation and removes the record' do
+ expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
+
+ expect { subject.perform }.to change { constraints_model.count }.by(-1)
+ end
+
+ it 'logs an appropriate message' do
+ expected_message = /Skipping #{constraint_name} validation since it does not exist/
+
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: expected_message))
+ end
+ end
+
+ context 'with error handling' do
+ before do
+ allow(connection).to receive(:execute).and_call_original
+
+ allow(connection).to receive(:execute)
+ .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/)
+ .and_raise(ActiveRecord::StatementInvalid)
+ end
+
+ context 'on production' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
+
+ it 'increases execution attempts' do
+ expect { subject.perform }.to change { validation.attempts }.by(1)
+
+ expect(validation.last_error).to be_present
+ expect(validation).not_to be_destroyed
+ end
+
+ it 'logs an error message including the constraint_name' do
+ expect(Gitlab::AppLogger)
+ .to receive(:error)
+ .with(a_hash_including(:message, :constraint_name))
+ .and_call_original
+
+ subject.perform
+ end
+ end
+
+ context 'on development' do
+ it 'also raises errors' do
+ expect { subject.perform }
+ .to raise_error(ActiveRecord::StatementInvalid)
+ .and change { validation.attempts }.by(1)
+
+ expect(validation.last_error).to be_present
+ expect(validation).not_to be_destroyed
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb
new file mode 100644
index 00000000000..6f0cede7130
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "index validators" do |validator, expected_result|
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:database_indexes) do
+ [
+ ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'],
+ ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'],
+ ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']
+ ]
+ end
+
+ let(:inconsistency_type) { validator.name.demodulize.underscore }
+
+ let(:database_name) { 'main' }
+
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+
+ let(:connection) { database_model.connection }
+
+ let(:schema) { connection.current_schema }
+
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
+
+ subject(:result) { validator.new(structure_file, database).execute }
+
+ before do
+ allow(connection).to receive(:select_rows).and_return(database_indexes)
+ end
+
+ it 'returns index inconsistencies' do
+ expect(result.map(&:object_name)).to match_array(expected_result)
+ expect(result.map(&:type)).to all(eql inconsistency_type)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb
new file mode 100644
index 00000000000..d5ecab0cb6b
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "schema objects assertions for" do |stmt_name|
+ let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt }
+ let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) }
+
+ describe '#name' do
+ it 'returns schema object name' do
+ expect(schema_object.name).to eq(name)
+ end
+ end
+
+ describe '#statement' do
+ it 'returns schema object statement' do
+ expect(schema_object.statement).to eq(statement)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb
new file mode 100644
index 00000000000..13a112275c2
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'trigger validators' do |validator, expected_result|
+ subject(:result) { validator.new(structure_file, database).execute }
+
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
+ let(:inconsistency_type) { validator.name.demodulize.underscore }
+ let(:database_name) { 'main' }
+ let(:schema) { 'public' }
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:connection) { database_model.connection }
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+
+ let(:database_triggers) do
+ [
+ ['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'],
+ ['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'],
+ ['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()']
+ ]
+ end
+
+ before do
+ allow(connection).to receive(:select_rows).and_return(database_triggers)
+ end
+
+ it 'returns trigger inconsistencies' do
+ expect(result.map(&:object_name)).to match_array(expected_result)
+ expect(result.map(&:type)).to all(eql inconsistency_type)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb
index a3e4379f4d3..18545698c27 100644
--- a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb
@@ -26,29 +26,4 @@ RSpec.shared_examples 'search results filtered by language' do
expect(blob_results.size).to eq(5)
expect(paths).to match_array(expected_paths)
end
-
- context 'when the search_blobs_language_aggregation feature flag is disabled' do
- before do
- stub_feature_flags(search_blobs_language_aggregation: false)
- end
-
- it 'does not filter by language', :sidekiq_inline, :aggregate_failures do
- expected_paths = %w[
- CHANGELOG
- CONTRIBUTING.md
- bar/branch-test.txt
- custom-highlighting/test.gitlab-custom
- files/ruby/popen.rb
- files/ruby/regex.rb
- files/ruby/version_info.rb
- files/whitespace
- encoding/test.txt
- files/markdown/ruby-style-guide.md
- ]
-
- paths = blob_results.map { |blob| blob.binary_path }
- expect(blob_results.size).to eq(10)
- expect(paths).to match_array(expected_paths)
- end
- end
end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
index d4802a19202..9873bab1caf 100644
--- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
@@ -27,18 +27,6 @@ RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events
expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params))
end
-
- context 'with route_hll_to_snowplow_phase2 disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'does not emit snowplow event' do
- track_action({ author: user1 }.merge(track_params))
-
- expect_no_snowplow_event
- end
- end
end
RSpec.shared_examples 'daily tracked issuable snowplow and service ping events with project' do
diff --git a/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb
new file mode 100644
index 00000000000..a42d1450e4d
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do
+ let(:randomhex) { 'randomhex' }
+
+ it 'check email domain' do
+ expect(subject.email).to end_with("@#{email_domain}")
+ end
+
+ it 'contains SecureRandom part' do
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+
+ expect(subject.username).to include("_#{randomhex}")
+ expect(subject.email).to include("_#{randomhex}@")
+ end
+
+ it 'email name is the same as username' do
+ expect(subject.email).to include("#{subject.username}@")
+ end
+
+ context 'when conflicts' do
+ let(:reserved_username) { "#{username_prefix}_#{randomhex}" }
+ let(:reserved_email) { "#{reserved_username}@#{email_domain}" }
+
+ shared_examples 'uniquifies username and email' do
+ it 'uniquifies username and email' do
+ expect(subject.username).to eq("#{reserved_username}1")
+ expect(subject.email).to include("#{subject.username}@")
+ end
+ end
+
+ context 'when username is reserved' do
+ context 'when username is reserved by user' do
+ before do
+ create(:user, username: reserved_username)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with top-level group namespace' do
+ before do
+ create(:group, path: reserved_username)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with top-level group namespace that includes upcased characters' do
+ before do
+ create(:group, path: reserved_username.upcase)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+ end
+
+ context 'when email is reserved' do
+ context 'when it conflicts with confirmed primary email' do
+ before do
+ create(:user, email: reserved_email)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with unconfirmed primary email' do
+ before do
+ create(:user, :unconfirmed, email: reserved_email)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+
+ context 'when it conflicts with confirmed secondary email' do
+ before do
+ create(:email, :confirmed, email: reserved_email)
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ include_examples 'uniquifies username and email'
+ end
+ end
+
+ context 'when email and username is reserved' do
+ before do
+ create(:user, email: reserved_email)
+ create(:user, username: "#{reserved_username}1")
+ allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
+ end
+
+ it 'uniquifies username and email' do
+ expect(subject.username).to eq("#{reserved_username}2")
+
+ expect(subject.email).to include("#{subject.username}@")
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/menus_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb
index 2c2cb362b07..ed3165079fb 100644
--- a/spec/support/shared_examples/lib/menus_shared_examples.rb
+++ b/spec/support/shared_examples/lib/menus_shared_examples.rb
@@ -37,3 +37,27 @@ RSpec.shared_examples_for 'pill_count formatted results' do
expect(pill_count).to eq('112.6k')
end
end
+
+RSpec.shared_examples_for 'serializable as super_sidebar_menu_args' do
+ let(:extra_attrs) { raise NotImplementedError }
+
+ it 'returns hash with provided attributes' do
+ expect(menu.serialize_as_menu_item_args).to eq({
+ title: menu.title,
+ link: menu.link,
+ active_routes: menu.active_routes,
+ container_html_options: menu.container_html_options,
+ **extra_attrs
+ })
+ end
+
+ it 'returns hash with an item_id' do
+ expect(menu.serialize_as_menu_item_args[:item_id]).not_to be_nil
+ end
+end
+
+RSpec.shared_examples_for 'not serializable as super_sidebar_menu_args' do
+ it 'returns nil' do
+ expect(menu.serialize_as_menu_item_args).to be_nil
+ end
+end
diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb
new file mode 100644
index 00000000000..92c51a03b31
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'User profile menu' do |title:, active_route:|
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:user) { build(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.has_items?).to be false
+ end
+
+ it 'renders the correct link' do
+ expect(subject.link).to match link
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq title
+ end
+
+ it 'defines correct active route' do
+ expect(subject.active_routes[:path]).to be active_route
+ end
+
+ it 'renders if user is logged in' do
+ expect(subject.render?).to be true
+ end
+
+ [:blocked, :banned].each do |trait|
+ context "when viewed user is #{trait}" do
+ let_it_be(:viewed_user) { build(:user, trait) }
+ let(:context) { Sidebars::Context.new(current_user: user, container: viewed_user) }
+
+ context 'when user is not logged in' do
+ it 'is not allowed to view the menu item' do
+ expect(described_class.new(Sidebars::Context.new(current_user: nil,
+ container: viewed_user)).render?).to be false
+ end
+ end
+
+ context 'when current user has permission' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_user_profile, viewed_user).and_return(true)
+ end
+
+ it 'is allowed to view the menu item' do
+ expect(described_class.new(context).render?).to be true
+ end
+ end
+
+ context 'when current user does not have permission' do
+ it 'is not allowed to view the menu item' do
+ expect(described_class.new(context).render?).to be false
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Followers/followees counts' do |symbol|
+ let_it_be(:current_user) { build(:user) }
+ let_it_be(:user) { build(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: current_user, container: user) }
+
+ subject { described_class.new(context) }
+
+ context 'when there are items' do
+ before do
+ allow(user).to receive(symbol).and_return([1, 2])
+ end
+
+ it 'renders the pill' do
+ expect(subject.has_pill?).to be(true)
+ end
+
+ it 'returns the count' do
+ expect(subject.pill_count).to be(2)
+ end
+ end
+
+ context 'when there are no items' do
+ it 'does not render the pill' do
+ expect(subject.has_pill?).to be(false)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb
new file mode 100644
index 00000000000..b91386d1935
--- /dev/null
+++ b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'User settings menu' do |link:, title:, icon:, active_routes:|
+ let_it_be(:user) { create(:user) }
+
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.has_items?).to be false
+ end
+
+ it 'renders the correct link' do
+ expect(subject.link).to match link
+ end
+
+ it 'renders the correct title' do
+ expect(subject.title).to eq title
+ end
+
+ it 'renders the correct icon' do
+ expect(subject.sprite_icon).to be icon
+ end
+
+ it 'defines correct active route' do
+ expect(subject.active_routes).to eq active_routes
+ end
+end
+
+RSpec.shared_examples 'User settings menu #render? method' do
+ describe '#render?' do
+ subject { described_class.new(context) }
+
+ context 'when user is logged in' do
+ let_it_be(:user) { build(:user) }
+ let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
+
+ it 'renders' do
+ expect(subject.render?).to be true
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
+
+ it 'does not render' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/mailers/export_csv_shared_examples.rb b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb
new file mode 100644
index 00000000000..731d7c810f9
--- /dev/null
+++ b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'export csv email' do |collection_type|
+ include_context 'gitlab email notification'
+
+ it 'attachment has csv mime type' do
+ expect(attachment.mime_type).to eq 'text/csv'
+ end
+
+ it 'generates a useful filename' do
+ expect(attachment.filename).to include(Date.today.year.to_s)
+ expect(attachment.filename).to include(collection_type)
+ expect(attachment.filename).to include('myproject')
+ expect(attachment.filename).to end_with('.csv')
+ end
+
+ it 'mentions number of objects and project name' do
+ expect(subject).to have_content '3'
+ expect(subject).to have_content empty_project.name
+ end
+
+ it "doesn't need to mention truncation by default" do
+ expect(subject).not_to have_content 'truncated'
+ end
+
+ context 'when truncated' do
+ let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } }
+
+ it 'mentions that the csv has been truncated' do
+ expect(subject).to have_content 'truncated'
+ end
+
+ it 'mentions the number of objects written and expected' do
+ expect(subject).to have_content "10 of 12 #{collection_type.humanize.downcase}"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb
index 3d765b6ca93..10f3263d4fc 100644
--- a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb
+++ b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb
@@ -10,3 +10,13 @@ RSpec.shared_examples 'having unique enum values' do
end
end
end
+
+RSpec.shared_examples 'having enum with nil value' do
+ it 'has enum with nil value' do
+ subject.public_send("#{attr_value}!")
+
+ expect(subject.public_send("#{attr}_for_database")).to be_nil
+ expect(subject.public_send("#{attr}?")).to eq(true)
+ expect(subject.class.public_send(attr_value)).to eq([subject])
+ end
+end
diff --git a/spec/support/shared_examples/models/ci/token_format_shared_examples.rb b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
new file mode 100644
index 00000000000..0272982e2d0
--- /dev/null
+++ b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'ensures runners_token is prefixed' do |factory|
+ subject(:record) { FactoryBot.build(factory) }
+
+ describe '#runners_token', feature_category: :system_access do
+ let(:runners_prefix) { RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX }
+
+ it 'generates runners_token which starts with runner prefix' do
+ expect(record.runners_token).to match(a_string_starting_with(runners_prefix))
+ end
+
+ context 'when record has an invalid token' do
+ subject(:record) { FactoryBot.build(factory, runners_token: invalid_runners_token) }
+
+ let(:invalid_runners_token) { "not_start_with_runners_prefix" }
+
+ it 'generates runners_token which starts with runner prefix' do
+ expect(record.runners_token).to match(a_string_starting_with(runners_prefix))
+ end
+
+ it 'changes the attribute values for runners_token and runners_token_encrypted' do
+ expect { record.runners_token }
+ .to change { record[:runners_token] }.from(invalid_runners_token).to(nil)
+ .and change { record[:runners_token_encrypted] }.from(nil)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
index 122774a9028..a26c20ccc61 100644
--- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -61,6 +61,20 @@ disabled_until: disabled_until)
# Nothing is missing
expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'causes all hooks to be considered executable' do
+ expect(find_hooks.executable.count).to eq(16)
+ end
+
+ it 'causes no hooks to be considered disabled' do
+ expect(find_hooks.disabled).to be_empty
+ end
+ end
end
describe '#executable?', :freeze_time do
@@ -108,6 +122,16 @@ disabled_until: disabled_until)
it 'has the correct state' do
expect(web_hook.executable?).to eq(executable)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is always executable' do
+ expect(web_hook).to be_executable
+ end
+ end
end
end
@@ -151,7 +175,7 @@ disabled_until: disabled_until)
context 'when we have exhausted the grace period' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
context 'when the hook is permanently disabled' do
@@ -172,6 +196,16 @@ disabled_until: disabled_until)
def run_expectation
expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not increment backoff count' do
+ expect { hook.failed! }.not_to change { hook.backoff_count }
+ end
+ end
end
end
end
@@ -181,6 +215,16 @@ disabled_until: disabled_until)
def run_expectation
expect { hook.failed! }.to change { hook.recent_failures }.by(1)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not increment recent failure count' do
+ expect { hook.failed! }.not_to change { hook.recent_failures }
+ end
+ end
end
end
@@ -189,6 +233,16 @@ disabled_until: disabled_until)
expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
end
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not disable the hook' do
+ expect { hook.disable! }.not_to change { hook.executable? }
+ end
+ end
+
it 'does nothing if the hook is already disabled' do
allow(hook).to receive(:permanently_disabled?).and_return(true)
@@ -210,7 +264,7 @@ disabled_until: disabled_until)
end
it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
- WebHook::FAILURE_THRESHOLD.times do
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do
hook.backoff!
expect(hook).not_to be_temporarily_disabled
end
@@ -221,13 +275,23 @@ disabled_until: disabled_until)
context 'when hook has been told to back off' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
hook.backoff!
end
it 'is true' do
expect(hook).to be_temporarily_disabled
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is false' do
+ expect(hook).not_to be_temporarily_disabled
+ end
+ end
end
end
@@ -244,6 +308,16 @@ disabled_until: disabled_until)
it 'is true' do
expect(hook).to be_permanently_disabled
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is false' do
+ expect(hook).not_to be_permanently_disabled
+ end
+ end
end
end
@@ -258,15 +332,31 @@ disabled_until: disabled_until)
end
it { is_expected.to eq :disabled }
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it { is_expected.to eq(:executable) }
+ end
end
context 'when hook has been backed off' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
hook.disabled_until = 1.hour.from_now
end
it { is_expected.to eq :temporarily_disabled }
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it { is_expected.to eq(:executable) }
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
index a4db4e25db3..9dec1a5056c 100644
--- a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb
@@ -171,15 +171,59 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do
end
describe "##{settings_attribute_name}=" do
- before do
- subgroup_settings.update!(settings_attribute_name => nil)
- group_settings.update!(settings_attribute_name => true)
+ using RSpec::Parameterized::TableSyntax
+
+ where(:parent_value, :current_subgroup_value, :new_subgroup_value, :expected_subgroup_value_after_update) do
+ true | nil | true | nil
+ true | nil | "true" | nil
+ true | false | true | true
+ true | false | "true" | true
+ true | true | false | false
+ true | true | "false" | false
+ false | nil | false | nil
+ false | nil | true | true
+ false | true | false | false
+ false | false | true | true
end
- it 'does not save the value locally when it matches the cascaded value' do
- subgroup_settings.update!(settings_attribute_name => true)
+ with_them do
+ before do
+ subgroup_settings.update!(settings_attribute_name => current_subgroup_value)
+ group_settings.update!(settings_attribute_name => parent_value)
+ end
+
+ it 'validates starting values from before block', :aggregate_failures do
+ expect(group_settings.reload.read_attribute(settings_attribute_name)).to eq(parent_value)
+ expect(subgroup_settings.reload.read_attribute(settings_attribute_name)).to eq(current_subgroup_value)
+ end
+
+ it 'does not save the value locally when it matches cascaded value', :aggregate_failures do
+ subgroup_settings.send("#{settings_attribute_name}=", new_subgroup_value)
+
+ # Verify dirty value
+ expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(expected_subgroup_value_after_update)
+
+ subgroup_settings.save!
- expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(nil)
+ # Verify persisted value
+ expect(subgroup_settings.reload.read_attribute(settings_attribute_name))
+ .to eq(expected_subgroup_value_after_update)
+ end
+
+ context 'when mass assigned' do
+ before do
+ subgroup_settings.attributes =
+ { settings_attribute_name => new_subgroup_value, "lock_#{settings_attribute_name}" => false }
+ end
+
+ it 'does not save the value locally when it matches cascaded value', :aggregate_failures do
+ subgroup_settings.save!
+
+ # Verify persisted value
+ expect(subgroup_settings.reload.read_attribute(settings_attribute_name))
+ .to eq(expected_subgroup_value_after_update)
+ end
+ end
end
end
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 5755b9a56b1..139deaaece9 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
@@ -17,6 +17,11 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
let(:amount) { 10 }
let(:increment) { Gitlab::Counters::Increment.new(amount: amount, ref: 3) }
let(:counter_key) { model.counter(attribute).key }
+ let(:returns_current) do
+ model.class.counter_attributes
+ .find { |a| a[:attribute] == attribute }
+ .fetch(:returns_current, false)
+ end
subject { model.increment_counter(attribute, increment) }
@@ -61,6 +66,33 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
+ describe '#increment_amount' do
+ it 'increases the egress in cache' do
+ model.increment_amount(attribute, 3)
+
+ expect(model.counter(attribute).get).to eq(3)
+ end
+ end
+
+ describe '#current_counter' do
+ let(:data_transfer_node) do
+ args = { project: project }
+ args[attribute] = 2
+ create(:project_data_transfer, **args)
+ end
+
+ it 'increases the amount in cache' do
+ if returns_current
+ incremented_by = 4
+ db_state = model.read_attribute(attribute)
+
+ model.send("increment_#{attribute}".to_sym, incremented_by)
+
+ expect(model.send(attribute)).to eq(db_state + incremented_by)
+ end
+ end
+ end
+
context 'when increment amount is 0' do
let(:amount) { 0 }
@@ -155,14 +187,24 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
describe '#update_counters_with_lease' do
- let(:increments) { { build_artifacts_size: 1, packages_size: 2 } }
+ let_it_be(:first_attribute) { counter_attributes.first }
+ let_it_be(:second_attribute) { counter_attributes.second }
+
+ let_it_be(:increments) do
+ increments_hash = {}
+
+ increments_hash[first_attribute] = 1
+ increments_hash[second_attribute] = 2
+
+ increments_hash
+ end
subject { model.update_counters_with_lease(increments) }
it 'updates counters of the record' do
expect { subject }
- .to change { model.reload.build_artifacts_size }.by(1)
- .and change { model.reload.packages_size }.by(2)
+ .to change { model.reload.send(first_attribute) }.by(1)
+ .and change { model.reload.send(second_attribute) }.by(2)
end
it_behaves_like 'obtaining lease to update database' do
diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
index 2e528f7996c..2dad35dc46e 100644
--- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
@@ -35,7 +35,6 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.to_s }
let(:action) { 'perform_integrations_action' }
let(:namespace) { project.namespace }
diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
index 848840ee297..24d114bbe23 100644
--- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
@@ -110,7 +110,7 @@ disabled_until: disabled_until)
context 'when we have exhausted the grace period' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
it 'does not disable the hook' do
@@ -131,7 +131,7 @@ disabled_until: disabled_until)
expect(hook).not_to be_temporarily_disabled
# Backing off
- WebHook::FAILURE_THRESHOLD.times do
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do
hook.backoff!
expect(hook).not_to be_temporarily_disabled
end
@@ -167,7 +167,7 @@ disabled_until: disabled_until)
context 'when hook has been backed off' do
before do
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
hook.disabled_until = 1.hour.from_now
end
diff --git a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
index cd6eb8c77fa..113dcc266fc 100644
--- a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
@@ -19,7 +19,7 @@ RSpec.shared_examples 'something that has web-hooks' do
context 'when there is a failed hook' do
before do
hook = create_hook
- hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
end
it { is_expected.to eq(true) }
@@ -83,7 +83,7 @@ RSpec.shared_examples 'something that has web-hooks' do
describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do
context 'when a value has not been stored' do
- it 'does not call #any_hook_failed?' do
+ it 'calls #any_hook_failed?' do
expect(object.get_web_hook_failure).to be_nil
expect(object).to receive(:any_hook_failed?).and_return(true)
diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
index 5eeefacdeb9..3f532629961 100644
--- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
+++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
@@ -290,6 +290,7 @@ RSpec.shared_examples 'value stream analytics label based stage' do
context 'when `ProjectLabel is given' do
let_it_be(:label) { create(:label) }
+ let(:expected_error) { s_('CycleAnalyticsStage|is not available for the selected group') }
it 'raises error when `ProjectLabel` is given for `start_event_label`' do
params = {
@@ -300,7 +301,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do
end_event_identifier: :issue_closed
}
- expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ stage = described_class.new(params)
+ expect(stage).to be_invalid
+ expect(stage.errors.messages_for(:start_event_label_id)).to eq([expected_error])
end
it 'raises error when `ProjectLabel` is given for `end_event_label`' do
@@ -312,7 +315,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do
end_event_label: label
}
- expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ stage = described_class.new(params)
+ expect(stage).to be_invalid
+ expect(stage.errors.messages_for(:end_event_label_id)).to eq([expected_error])
end
end
end
diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
index 7dfdd24177e..0cf109ce5c5 100644
--- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
@@ -3,7 +3,6 @@
RSpec.shared_examples Integrations::BaseSlashCommands do
describe "Associations" do
it { is_expected.to respond_to :token }
- it { is_expected.to have_many :chat_names }
end
describe 'default values' do
@@ -85,7 +84,7 @@ RSpec.shared_examples Integrations::BaseSlashCommands do
end
context 'when the user is authenticated' do
- let!(:chat_name) { create(:chat_name, integration: subject) }
+ let!(:chat_name) { create(:chat_name) }
let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
subject do
diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
index 5be0f6349ea..78591482696 100644
--- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
+++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
@@ -231,4 +231,20 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") }
end
end
+
+ describe '#empty?' do
+ subject { component_file_with_architecture.empty? }
+
+ context 'with a non-empty component' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with an empty component' do
+ before do
+ component_file_with_architecture.update! size: 0
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
index 3caf58da4d2..f1af1760e8d 100644
--- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb
@@ -19,7 +19,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do
end
end
-RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''|
+RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: '', default: false|
using RSpec::Parameterized::TableSyntax
context 'when ci_cd_settings is nil' do
@@ -28,7 +28,7 @@ RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''|
end
it 'returns false' do
- expect(project.send("#{prefix}#{delegated_method}")).to be(false)
+ expect(project.send("#{prefix}#{delegated_method}")).to be(default)
end
end
diff --git a/spec/support/shared_examples/observability/csp_shared_examples.rb b/spec/support/shared_examples/observability/csp_shared_examples.rb
index 0cd211f69eb..9d6e7e75f4d 100644
--- a/spec/support/shared_examples/observability/csp_shared_examples.rb
+++ b/spec/support/shared_examples/observability/csp_shared_examples.rb
@@ -31,19 +31,19 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
let(:observability_url) { Gitlab::Observability.observability_url }
let(:signin_url) do
Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
- '/users/sign_in')
+ '/users/sign_in')
end
let(:oauth_url) do
Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
- '/oauth/authorize')
+ '/oauth/authorize')
end
before do
setup_csp_for_controller(controller_class, csp, any_time: true)
group.add_developer(user)
login_as(user)
- allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(true)
+ stub_feature_flags(observability_group_tab: true)
end
subject do
@@ -67,7 +67,7 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
end
before do
- allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(false)
+ stub_feature_flags(observability_group_tab: false)
end
it 'does not add observability urls to the csp header' do
@@ -76,23 +76,6 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe
end
end
- context 'when checking if observability is enabled' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src 'https://something.test'
- end
- end
-
- it 'check access for a given user and group' do
- allow(Gitlab::Observability).to receive(:observability_enabled?)
-
- get tested_path
-
- expect(Gitlab::Observability).to have_received(:observability_enabled?)
- .with(user, group).at_least(:once)
- end
- end
-
context 'when frame-src exists in the CSP config' do
let(:csp) do
ActionDispatch::ContentSecurityPolicy.new do |p|
diff --git a/spec/support/shared_examples/observability/embed_observabilities_examples.rb b/spec/support/shared_examples/observability/embed_observabilities_examples.rb
new file mode 100644
index 00000000000..c8d4e9e0d7e
--- /dev/null
+++ b/spec/support/shared_examples/observability/embed_observabilities_examples.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'embeds observability' do
+ it 'renders iframe in description' do
+ page.within('.description') do
+ expect_observability_iframe(page.html)
+ end
+ end
+
+ it 'renders iframe in comment' do
+ expect(page).not_to have_css('.note-text')
+
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: observable_url)
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note-text') do
+ expect_observability_iframe(page.html)
+ end
+ end
+end
+
+RSpec.shared_examples 'does not embed observability' do
+ it 'does not render iframe in description' do
+ page.within('.description') do
+ expect_observability_iframe(page.html, to_be_nil: true)
+ end
+ end
+
+ it 'does not render iframe in comment' do
+ expect(page).not_to have_css('.note-text')
+
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: observable_url)
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note-text') do
+ expect_observability_iframe(page.html, to_be_nil: true)
+ end
+ end
+end
+
+def expect_observability_iframe(html, to_be_nil: false)
+ iframe = Nokogiri::HTML.parse(html).at_css('#observability-ui-iframe')
+
+ expect(html).to include(observable_url)
+
+ if to_be_nil
+ expect(iframe).to be_nil
+ else
+ expect(iframe).not_to be_nil
+ iframe_src = "#{expected_observable_url}&theme=light&username=#{user.username}&kiosk=inline-embed"
+ expect(iframe.attributes['src'].value).to eq(iframe_src)
+ end
+end
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 9ec1b8b3f5a..d1f5a01b10c 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -401,3 +401,24 @@ RSpec.shared_examples 'package access with repository disabled' do
it { is_expected.to be_allowed(:read_package) }
end
+
+RSpec.shared_examples 'equivalent project policy abilities' do
+ where(:project_visibility, :user_role_on_project) do
+ project_visibilities = [:public, :internal, :private]
+ user_role_on_project = [:anonymous, :non_member, :guest, :reporter, :developer, :maintainer, :owner, :admin]
+ project_visibilities.product(user_role_on_project)
+ end
+
+ with_them do
+ it 'evaluates the same' do
+ project = public_send("#{project_visibility}_project")
+ current_user = public_send(user_role_on_project)
+ enable_admin_mode!(current_user) if user_role_on_project == :admin
+ policy = ProjectPolicy.new(current_user, project)
+ old_permissions = policy.allowed?(old_policy)
+ new_permissions = policy.allowed?(new_policy)
+
+ expect(old_permissions).to eq new_permissions
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
index 07fde7d3f35..ceb57fca786 100644
--- a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
+++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
@@ -1,94 +1,99 @@
# frozen_string_literal: true
-RSpec.shared_examples 'GET request permissions for admin mode' do
- it_behaves_like 'GET request permissions for admin mode when user'
- it_behaves_like 'GET request permissions for admin mode when admin'
+RSpec.shared_examples 'GET request permissions for admin mode' do |failed_status_code = :forbidden|
+ it_behaves_like 'GET request permissions for admin mode when user', failed_status_code
+ it_behaves_like 'GET request permissions for admin mode when admin', failed_status_code
end
-RSpec.shared_examples 'PUT request permissions for admin mode' do |params|
- it_behaves_like 'PUT request permissions for admin mode when user', params
- it_behaves_like 'PUT request permissions for admin mode when admin', params
+RSpec.shared_examples 'PUT request permissions for admin mode' do |failed_status_code = :forbidden|
+ it_behaves_like 'PUT request permissions for admin mode when user', failed_status_code
+ it_behaves_like 'PUT request permissions for admin mode when admin', failed_status_code
end
-RSpec.shared_examples 'POST request permissions for admin mode' do |params|
- it_behaves_like 'POST request permissions for admin mode when user', params
- it_behaves_like 'POST request permissions for admin mode when admin', params
+RSpec.shared_examples 'POST request permissions for admin mode' do |failed_status_code = :forbidden|
+ it_behaves_like 'POST request permissions for admin mode when user', failed_status_code
+ it_behaves_like 'POST request permissions for admin mode when admin', failed_status_code
end
-RSpec.shared_examples 'DELETE request permissions for admin mode' do
- it_behaves_like 'DELETE request permissions for admin mode when user'
- it_behaves_like 'DELETE request permissions for admin mode when admin'
+RSpec.shared_examples 'DELETE request permissions for admin mode' do |success_status_code: :no_content,
+ failed_status_code: :forbidden|
+
+ it_behaves_like 'DELETE request permissions for admin mode when user', failed_status_code
+ it_behaves_like 'DELETE request permissions for admin mode when admin', success_status_code: success_status_code,
+ failed_status_code: failed_status_code
end
-RSpec.shared_examples 'GET request permissions for admin mode when user' do
+RSpec.shared_examples 'GET request permissions for admin mode when user' do |failed_status_code = :forbidden|
subject { get api(path, current_user, admin_mode: admin_mode) }
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', true, failed_status_code
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'GET request permissions for admin mode when admin' do
+RSpec.shared_examples 'GET request permissions for admin mode when admin' do |failed_status_code = :forbidden|
subject { get api(path, current_user, admin_mode: admin_mode) }
let_it_be(:current_user) { create(:admin) }
it_behaves_like 'admin mode on', true, :ok
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params|
+RSpec.shared_examples 'PUT request permissions for admin mode when user' do |failed_status_code = :forbidden|
subject { put api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', true, failed_status_code
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params|
+RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |failed_status_code = :forbidden|
subject { put api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:admin) }
it_behaves_like 'admin mode on', true, :ok
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'POST request permissions for admin mode when user' do |params|
+RSpec.shared_examples 'POST request permissions for admin mode when user' do |failed_status_code = :forbidden|
subject { post api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', true, failed_status_code
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params|
+RSpec.shared_examples 'POST request permissions for admin mode when admin' do |failed_status_code = :forbidden|
subject { post api(path, current_user, admin_mode: admin_mode), params: params }
let_it_be(:current_user) { create(:admin) }
it_behaves_like 'admin mode on', true, :created
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'DELETE request permissions for admin mode when user' do
+RSpec.shared_examples 'DELETE request permissions for admin mode when user' do |failed_status_code = :forbidden|
subject { delete api(path, current_user, admin_mode: admin_mode) }
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'admin mode on', true, :forbidden
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', true, failed_status_code
+ it_behaves_like 'admin mode on', false, failed_status_code
end
-RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do
+RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do |success_status_code: :no_content,
+ failed_status_code: :forbidden|
+
subject { delete api(path, current_user, admin_mode: admin_mode) }
let_it_be(:current_user) { create(:admin) }
- it_behaves_like 'admin mode on', true, :no_content
- it_behaves_like 'admin mode on', false, :forbidden
+ it_behaves_like 'admin mode on', true, success_status_code
+ it_behaves_like 'admin mode on', false, failed_status_code
end
RSpec.shared_examples "admin mode on" do |admin_mode, status|
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index 6d29076da0f..66554f18e80 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -165,3 +165,29 @@ RSpec.shared_examples 'Debian packages write endpoint' do |desired_behavior, suc
it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic
end
+
+RSpec.shared_examples 'Debian packages index endpoint' do |success_body|
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body
+
+ context 'when no ComponentFile is found' do
+ let(:target_component_name) { component.name + FFaker::Lorem.word }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/
+ end
+end
+
+RSpec.shared_examples 'Debian packages index sha256 endpoint' do |success_body|
+ it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body
+
+ context 'with empty checksum' do
+ let(:target_sha256) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/
+ end
+
+ context 'when ComponentFile is not found' do
+ let(:target_component_name) { component.name + FFaker::Lorem.word }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /^{"message":"404 Not Found"}$/
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
index f577e2ad323..a90fa06d458 100644
--- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
@@ -123,18 +123,6 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name,
expect_snowplow_event(category: 'Notes::CreateService', action: 'execute', label: 'note', value: anything)
end
- context 'with notes_create_service_tracking feature flag disabled' do
- before do
- stub_feature_flags(notes_create_service_tracking: false)
- end
-
- it 'does not track Notes::CreateService events', :snowplow do
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' }
-
- expect_no_snowplow_event(category: 'Notes::CreateService', action: 'execute')
- end
- end
-
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
index f2002de4b55..797c5be802e 100644
--- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
@@ -135,7 +135,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
context 'the hook is backed-off' do
before do
- WebHook::FAILURE_THRESHOLD.times { hook.backoff! }
+ WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.backoff! }
hook.backoff!
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index b55639a6b82..ace76b5ef84 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -507,55 +507,118 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
it_behaves_like 'returning response status', status
end
- shared_examples 'handling different package names, visibilities and user roles' do
- where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | :public | :anonymous | :accept | :ok
- :scoped_naming_convention | :public | :guest | :accept | :ok
- :scoped_naming_convention | :public | :reporter | :accept | :ok
- :scoped_no_naming_convention | :public | :anonymous | :accept | :ok
- :scoped_no_naming_convention | :public | :guest | :accept | :ok
- :scoped_no_naming_convention | :public | :reporter | :accept | :ok
- :unscoped | :public | :anonymous | :accept | :ok
- :unscoped | :public | :guest | :accept | :ok
- :unscoped | :public | :reporter | :accept | :ok
- :non_existing | :public | :anonymous | :reject | :not_found
- :non_existing | :public | :guest | :reject | :not_found
- :non_existing | :public | :reporter | :reject | :not_found
-
- :scoped_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_naming_convention | :private | :reporter | :accept | :ok
- :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :private | :reporter | :accept | :ok
- :unscoped | :private | :anonymous | :reject | :not_found
- :unscoped | :private | :guest | :reject | :forbidden
- :unscoped | :private | :reporter | :accept | :ok
- :non_existing | :private | :anonymous | :reject | :not_found
- :non_existing | :private | :guest | :reject | :forbidden
- :non_existing | :private | :reporter | :reject | :not_found
-
- :scoped_naming_convention | :internal | :anonymous | :reject | :not_found
- :scoped_naming_convention | :internal | :guest | :accept | :ok
- :scoped_naming_convention | :internal | :reporter | :accept | :ok
- :scoped_no_naming_convention | :internal | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :internal | :guest | :accept | :ok
- :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
- :unscoped | :internal | :anonymous | :reject | :not_found
- :unscoped | :internal | :guest | :accept | :ok
- :unscoped | :internal | :reporter | :accept | :ok
- :non_existing | :internal | :anonymous | :reject | :not_found
- :non_existing | :internal | :guest | :reject | :not_found
- :non_existing | :internal | :reporter | :reject | :not_found
+ shared_examples 'handling all conditions' do
+ where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | :public | nil | :accept | :ok
+ nil | :scoped_no_naming_convention | :public | nil | :accept | :ok
+ nil | :unscoped | :public | nil | :accept | :ok
+ nil | :non_existing | :public | nil | :reject | :not_found
+ nil | :scoped_naming_convention | :private | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | :private | nil | :reject | :not_found
+ nil | :unscoped | :private | nil | :reject | :not_found
+ nil | :non_existing | :private | nil | :reject | :not_found
+ nil | :scoped_naming_convention | :internal | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | :internal | nil | :reject | :not_found
+ nil | :unscoped | :internal | nil | :reject | :not_found
+ nil | :non_existing | :internal | nil | :reject | :not_found
+
+ :oauth | :scoped_naming_convention | :public | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | :public | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :public | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :public | :reporter | :accept | :ok
+ :oauth | :unscoped | :public | :guest | :accept | :ok
+ :oauth | :unscoped | :public | :reporter | :accept | :ok
+ :oauth | :non_existing | :public | :guest | :reject | :not_found
+ :oauth | :non_existing | :public | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :private | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :private | :reporter | :accept | :ok
+ :oauth | :unscoped | :private | :guest | :reject | :forbidden
+ :oauth | :unscoped | :private | :reporter | :accept | :ok
+ :oauth | :non_existing | :private | :guest | :reject | :forbidden
+ :oauth | :non_existing | :private | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | :internal | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | :internal | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :internal | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
+ :oauth | :unscoped | :internal | :guest | :accept | :ok
+ :oauth | :unscoped | :internal | :reporter | :accept | :ok
+ :oauth | :non_existing | :internal | :guest | :reject | :not_found
+ :oauth | :non_existing | :internal | :reporter | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | :public | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :public | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | :public | :guest | :accept | :ok
+ :personal_access_token | :unscoped | :public | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | :public | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | :public | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :private | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :private | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :private | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :private | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | :internal | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | :internal | :guest | :accept | :ok
+ :personal_access_token | :unscoped | :internal | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | :internal | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | :internal | :reporter | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | :public | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :public | :developer | :accept | :ok
+ :job_token | :unscoped | :public | :developer | :accept | :ok
+ :job_token | :non_existing | :public | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | :private | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :private | :developer | :accept | :ok
+ :job_token | :unscoped | :private | :developer | :accept | :ok
+ :job_token | :non_existing | :private | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | :internal | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :internal | :developer | :accept | :ok
+ :job_token | :unscoped | :internal | :developer | :accept | :ok
+ :job_token | :non_existing | :internal | :developer | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :unscoped | :public | nil | :accept | :ok
+ :deploy_token | :non_existing | :public | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :unscoped | :private | nil | :accept | :ok
+ :deploy_token | :non_existing | :private | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :unscoped | :internal | nil | :accept | :ok
+ :deploy_token | :non_existing | :internal | nil | :reject | :not_found
end
with_them do
- let(:anonymous) { user_role == :anonymous }
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.plaintext_token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
+ end
+ end
- subject { get(url, headers: anonymous ? {} : headers) }
+ subject { get(url, headers: headers) }
before do
- project.send("add_#{user_role}", user) unless anonymous
+ project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
end
@@ -571,20 +634,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
end
end
- shared_examples 'handling all conditions' do
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.plaintext_token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
-
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
- end
-
context 'with a group namespace' do
it_behaves_like 'handling all conditions'
end
@@ -599,7 +648,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
end
RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
- using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
let_it_be(:tag_name) { 'test' }
@@ -617,82 +665,10 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
it_behaves_like 'returning response status', status
end
- shared_examples 'handling different package names, visibilities and user roles' do
- where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_naming_convention | :public | :developer | :accept | :ok
- :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :public | :developer | :accept | :ok
- :unscoped | :public | :anonymous | :reject | :forbidden
- :unscoped | :public | :guest | :reject | :forbidden
- :unscoped | :public | :developer | :accept | :ok
- :non_existing | :public | :anonymous | :reject | :forbidden
- :non_existing | :public | :guest | :reject | :forbidden
- :non_existing | :public | :developer | :reject | :not_found
-
- :scoped_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_naming_convention | :private | :developer | :accept | :ok
- :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :private | :developer | :accept | :ok
- :unscoped | :private | :anonymous | :reject | :not_found
- :unscoped | :private | :guest | :reject | :forbidden
- :unscoped | :private | :developer | :accept | :ok
- :non_existing | :private | :anonymous | :reject | :not_found
- :non_existing | :private | :guest | :reject | :forbidden
- :non_existing | :private | :developer | :reject | :not_found
-
- :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_naming_convention | :internal | :developer | :accept | :ok
- :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :developer | :accept | :ok
- :unscoped | :internal | :anonymous | :reject | :forbidden
- :unscoped | :internal | :guest | :reject | :forbidden
- :unscoped | :internal | :developer | :accept | :ok
- :non_existing | :internal | :anonymous | :reject | :forbidden
- :non_existing | :internal | :guest | :reject | :forbidden
- :non_existing | :internal | :developer | :reject | :not_found
- end
-
- with_them do
- let(:anonymous) { user_role == :anonymous }
-
- subject { put(url, env: env, headers: headers) }
-
- before do
- project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: visibility.to_s)
- end
-
- example_name = "#{params[:expected_result]} create package tag request"
- status = params[:expected_status]
-
- if scope == :instance && params[:package_name_type] != :scoped_naming_convention
- example_name = 'reject create package tag request'
- status = :not_found
- end
-
- it_behaves_like example_name, status: status
- end
- end
-
shared_examples 'handling all conditions' do
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.plaintext_token) }
+ subject { put(url, env: env, headers: headers) }
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
-
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
+ it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :create, scope: scope
end
context 'with a group namespace' do
@@ -709,7 +685,6 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
end
RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
- using RSpec::Parameterized::TableSyntax
include_context 'set package name from package name type'
let_it_be(:package_tag) { create(:packages_tag, package: package) }
@@ -725,82 +700,10 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
it_behaves_like 'returning response status', status
end
- shared_examples 'handling different package names, visibilities and user roles' do
- where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_naming_convention | :public | :maintainer | :accept | :ok
- :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :public | :maintainer | :accept | :ok
- :unscoped | :public | :anonymous | :reject | :forbidden
- :unscoped | :public | :guest | :reject | :forbidden
- :unscoped | :public | :maintainer | :accept | :ok
- :non_existing | :public | :anonymous | :reject | :forbidden
- :non_existing | :public | :guest | :reject | :forbidden
- :non_existing | :public | :maintainer | :reject | :not_found
-
- :scoped_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_naming_convention | :private | :maintainer | :accept | :ok
- :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
- :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :private | :maintainer | :accept | :ok
- :unscoped | :private | :anonymous | :reject | :not_found
- :unscoped | :private | :guest | :reject | :forbidden
- :unscoped | :private | :maintainer | :accept | :ok
- :non_existing | :private | :anonymous | :reject | :not_found
- :non_existing | :private | :guest | :reject | :forbidden
- :non_existing | :private | :maintainer | :reject | :not_found
-
- :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_naming_convention | :internal | :maintainer | :accept | :ok
- :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
- :scoped_no_naming_convention | :internal | :maintainer | :accept | :ok
- :unscoped | :internal | :anonymous | :reject | :forbidden
- :unscoped | :internal | :guest | :reject | :forbidden
- :unscoped | :internal | :maintainer | :accept | :ok
- :non_existing | :internal | :anonymous | :reject | :forbidden
- :non_existing | :internal | :guest | :reject | :forbidden
- :non_existing | :internal | :maintainer | :reject | :not_found
- end
-
- with_them do
- let(:anonymous) { user_role == :anonymous }
-
- subject { delete(url, headers: headers) }
-
- before do
- project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: visibility.to_s)
- end
-
- example_name = "#{params[:expected_result]} delete package tag request"
- status = params[:expected_status]
-
- if scope == :instance && params[:package_name_type] != :scoped_naming_convention
- example_name = 'reject delete package tag request'
- status = :not_found
- end
-
- it_behaves_like example_name, status: status
- end
- end
-
shared_examples 'handling all conditions' do
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.plaintext_token) }
-
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
-
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
+ subject { delete(url, headers: headers) }
- it_behaves_like 'handling different package names, visibilities and user roles'
- end
+ it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :delete, scope: scope
end
context 'with a group namespace' do
@@ -815,3 +718,141 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
end
end
end
+
+RSpec.shared_examples 'handling different package names, visibilities and user roles for tags create or delete' do |action:, scope: :project|
+ using RSpec::Parameterized::TableSyntax
+
+ role = action == :create ? :developer : :maintainer
+
+ where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :oauth | :scoped_naming_convention | :public | nil | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :public | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :public | role | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :public | nil | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :public | role | :accept | :ok
+ :oauth | :unscoped | :public | nil | :reject | :forbidden
+ :oauth | :unscoped | :public | :guest | :reject | :forbidden
+ :oauth | :unscoped | :public | role | :accept | :ok
+ :oauth | :non_existing | :public | nil | :reject | :forbidden
+ :oauth | :non_existing | :public | :guest | :reject | :forbidden
+ :oauth | :non_existing | :public | role | :reject | :not_found
+ :oauth | :scoped_naming_convention | :private | nil | :reject | :not_found
+ :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :private | role | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :private | nil | :reject | :not_found
+ :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :private | role | :accept | :ok
+ :oauth | :unscoped | :private | nil | :reject | :not_found
+ :oauth | :unscoped | :private | :guest | :reject | :forbidden
+ :oauth | :unscoped | :private | role | :accept | :ok
+ :oauth | :non_existing | :private | nil | :reject | :not_found
+ :oauth | :non_existing | :private | :guest | :reject | :forbidden
+ :oauth | :non_existing | :private | role | :reject | :not_found
+ :oauth | :scoped_naming_convention | :internal | nil | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :internal | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | :internal | role | :accept | :ok
+ :oauth | :scoped_no_naming_convention | :internal | nil | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | :internal | role | :accept | :ok
+ :oauth | :unscoped | :internal | nil | :reject | :forbidden
+ :oauth | :unscoped | :internal | :guest | :reject | :forbidden
+ :oauth | :unscoped | :internal | role | :accept | :ok
+ :oauth | :non_existing | :internal | nil | :reject | :forbidden
+ :oauth | :non_existing | :internal | :guest | :reject | :forbidden
+ :oauth | :non_existing | :internal | role | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | :public | nil | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :public | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :public | role | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :public | nil | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :public | role | :accept | :ok
+ :personal_access_token | :unscoped | :public | nil | :reject | :forbidden
+ :personal_access_token | :unscoped | :public | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :public | role | :accept | :ok
+ :personal_access_token | :non_existing | :public | nil | :reject | :forbidden
+ :personal_access_token | :non_existing | :public | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :public | role | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :private | nil | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :private | role | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :private | nil | :reject | :not_found
+ :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :private | role | :accept | :ok
+ :personal_access_token | :unscoped | :private | nil | :reject | :not_found
+ :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :private | role | :accept | :ok
+ :personal_access_token | :non_existing | :private | nil | :reject | :not_found
+ :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :private | role | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | :internal | nil | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | :internal | role | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | :internal | nil | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | :internal | role | :accept | :ok
+ :personal_access_token | :unscoped | :internal | nil | :reject | :forbidden
+ :personal_access_token | :unscoped | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | :internal | role | :accept | :ok
+ :personal_access_token | :non_existing | :internal | nil | :reject | :forbidden
+ :personal_access_token | :non_existing | :internal | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | :internal | role | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | :public | role | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :public | role | :accept | :ok
+ :job_token | :unscoped | :public | role | :accept | :ok
+ :job_token | :non_existing | :public | role | :reject | :not_found
+ :job_token | :scoped_naming_convention | :private | role | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :private | role | :accept | :ok
+ :job_token | :unscoped | :private | role | :accept | :ok
+ :job_token | :non_existing | :private | role | :reject | :not_found
+ :job_token | :scoped_naming_convention | :internal | role | :accept | :ok
+ :job_token | :scoped_no_naming_convention | :internal | role | :accept | :ok
+ :job_token | :unscoped | :internal | role | :accept | :ok
+ :job_token | :non_existing | :internal | role | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok
+ :deploy_token | :unscoped | :public | nil | :accept | :ok
+ :deploy_token | :non_existing | :public | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok
+ :deploy_token | :unscoped | :private | nil | :accept | :ok
+ :deploy_token | :non_existing | :private | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok
+ :deploy_token | :unscoped | :internal | nil | :accept | :ok
+ :deploy_token | :non_existing | :internal | nil | :reject | :not_found
+ end
+
+ with_them do
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.plaintext_token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ end
+ end
+
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: visibility.to_s)
+ end
+
+ example_name = "#{params[:expected_result]} #{action} package tag request"
+ status = params[:expected_status]
+
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = "reject #{action} package tag request"
+ status = :not_found
+ end
+
+ it_behaves_like example_name, status: status
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index 17d8b9c7fab..7cafe8bb368 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -36,6 +36,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -72,6 +73,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s
with_them do
let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') }
let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -140,6 +142,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |e
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -207,6 +210,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
@@ -277,6 +281,7 @@ RSpec.shared_examples 'handling nuget search requests' do |example_names_with_st
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index 6065b1163c4..9bd430c3b4f 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
@@ -254,6 +254,13 @@ RSpec.shared_examples 'pypi simple API endpoint' do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) do
+ if user_role == :anonymous || (visibility_level == :public && !user_token)
+ snowplow_context
+ else
+ snowplow_context.merge(user: user)
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
@@ -269,7 +276,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do
let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" }
let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_pypi_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context.merge({ project: project, user: user }) }
it_behaves_like 'PyPI package versions', :developer, :success
end
diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb
index 40843ccbd15..fad5211fc59 100644
--- a/spec/support/shared_examples/requests/api/status_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb
@@ -21,6 +21,23 @@ RSpec.shared_examples '400 response' do
end
end
+RSpec.shared_examples '401 response' do
+ let(:message) { nil }
+
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 401' do
+ expect(response).to have_gitlab_http_status(:unauthorized)
+
+ if message.present?
+ expect(json_response['message']).to eq(message)
+ end
+ end
+end
+
RSpec.shared_examples '403 response' do
before do
# Fires the request
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 642930dd982..4a7a7492398 100644
--- a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb
@@ -7,40 +7,14 @@ RSpec.shared_examples 'applications controller - GET #show' do
expect(response).to render_template :show
end
-
- 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
- get show_path
-
- expect(assigns[:created]).to eq(true)
- end
- 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
-
- expect(assigns[:created]).to eq(false)
- end
- end
end
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)
+ it "sets `@created` instance variable to `true`" do
create_application
- expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true)
+ expect(assigns[:created]).to eq(true)
end
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 a3042ac2e26..5abdac07431 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
@@ -29,26 +29,76 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
- let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
- let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
- let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
- let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
- let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
- let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
-
- def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
+ let_it_be(:component_file_old_main_amd64) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'a') } # destroyed
+
+ let_it_be(:component_file_oldest_kept_contrib_all) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'b') } # oldest kept
+ let_it_be(:component_file_oldest_kept_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # oldest kept
+ let_it_be(:component_file_recent_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'd') } # kept, less than 1 hour ago
+
+ let_it_be(:component_file_empty_contrib_all_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z') } # oldest kept
+ let_it_be(:component_file_empty_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z') } # touched, as last empty
+ let_it_be(:component_file_recent_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'f') } # kept, less than 1 hour ago
+
+ let(:pool_prefix) do
+ prefix = "pool/#{distribution.codename}"
+ prefix += "/#{project.id}" if container_type == :group
+ prefix += "/#{package.name[0]}/#{package.name}/#{package.version}"
+ prefix
+ end
+
+ let(:expected_main_amd64_di_content) do
+ <<~MAIN_AMD64_DI_CONTENT
+ Section: misc
+ Priority: extra
+ Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb
+ Size: 409600
+ SHA256: #{package.package_files.with_debian_file_type(:udeb).first.file_sha256}
+ MAIN_AMD64_DI_CONTENT
+ end
+
+ let(:expected_main_amd64_di_sha256) { Digest::SHA256.hexdigest(expected_main_amd64_di_content) }
+ let!(:component_file_old_main_amd64_di) do # touched
+ create("debian_#{container_type}_component_file", :di_packages, component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: expected_main_amd64_di_sha256).tap do |cf|
+ cf.update! file: CarrierWaveStringFile.new(expected_main_amd64_di_content), size: expected_main_amd64_di_content.size
+ end
+ end
+
+ def check_component_file(
+ release_date, component_name, component_file_type, architecture_name, expected_content,
+ updated: true, id_of: nil
+ )
component_file = distribution
.component_files
.with_component_name(component_name)
.with_file_type(component_file_type)
.with_architecture_name(architecture_name)
+ .with_compression_type(nil)
.order_updated_asc
.last
+ if expected_content.nil?
+ expect(component_file).to be_nil
+ return
+ end
+
expect(component_file).not_to be_nil
- expect(component_file.updated_at).to eq(release_date)
- unless expected_content.nil?
+ if id_of
+ expect(component_file&.id).to eq(id_of.id)
+ else
+ # created
+ expect(component_file&.id).to be > component_file_old_main_amd64_di.id
+ end
+
+ if updated
+ expect(component_file.updated_at).to eq(release_date)
+ else
+ expect(component_file.updated_at).not_to eq(release_date)
+ end
+
+ if expected_content == ''
+ expect(component_file.size).to eq(0)
+ else
expect(expected_content).not_to include('MD5')
component_file.file.use_file do |file_path|
expect(File.read(file_path)).to eq(expected_content)
@@ -57,30 +107,23 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
end
it 'generates Debian distribution and component files', :aggregate_failures do
- current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
+ current_time = Time.utc(2020, 1, 25, 15, 17, 19)
travel_to(current_time) do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- components_count = 2
- architectures_count = 3
-
- initial_count = 6
- destroyed_count = 2
- updated_count = 1
- created_count = components_count * (architectures_count * 2 + 1) - updated_count
+ initial_count = 8
+ destroyed_count = 1
+ created_count = 4 # main_amd64 + main_sources + empty contrib_all + empty contrib_amd64
expect { subject }
.to not_change { Packages::Package.count }
.and not_change { Packages::PackageFile.count }
.and change { distribution.reload.updated_at }.to(current_time.round)
.and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
- .and change { component_file1.reload.updated_at }.to(current_time.round)
+ .and change { component_file_old_main_amd64_di.reload.updated_at }.to(current_time.round)
package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a
- pool_prefix = "pool/#{distribution.codename}"
- pool_prefix += "/#{project.id}" if container_type == :group
- pool_prefix += "/#{package.name[0]}/#{package.name}/#{package.version}"
expected_main_amd64_content = <<~EOF
Package: libsample0
Source: #{package.name}
@@ -120,17 +163,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
SHA256: #{package_files[3].file_sha256}
EOF
- expected_main_amd64_di_content = <<~EOF
- Section: misc
- Priority: extra
- Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb
- Size: 409600
- SHA256: #{package_files[4].file_sha256}
- EOF
-
expected_main_sources_content = <<~EOF
Package: #{package.name}
- Binary: sample-dev, libsample0, sample-udeb
+ Binary: sample-dev, libsample0, sample-udeb, sample-ddeb
Version: #{package.version}
Maintainer: #{package_files[1].debian_fields['Maintainer']}
Build-Depends: debhelper-compat (= 13)
@@ -139,13 +174,13 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Format: 3.0 (native)
Files:
#{package_files[1].file_md5} #{package_files[1].size} #{package_files[1].file_name}
- d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz
+ #{package_files[0].file_md5} 964 #{package_files[0].file_name}
Checksums-Sha256:
#{package_files[1].file_sha256} #{package_files[1].size} #{package_files[1].file_name}
- 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
+ #{package_files[0].file_sha256} 964 #{package_files[0].file_name}
Checksums-Sha1:
#{package_files[1].file_sha1} #{package_files[1].size} #{package_files[1].file_name}
- c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
+ #{package_files[0].file_sha1} 964 #{package_files[0].file_name}
Homepage: #{package_files[1].debian_fields['Homepage']}
Section: misc
Priority: extra
@@ -157,42 +192,38 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
check_component_file(current_time.round, 'main', :di_packages, 'all', nil)
- check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content)
+ check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content, id_of: component_file_old_main_amd64_di)
check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil)
check_component_file(current_time.round, 'main', :sources, nil, expected_main_sources_content)
- check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
- check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'all', '')
+ check_component_file(current_time.round, 'contrib', :packages, 'amd64', '')
check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil)
- check_component_file(current_time.round, 'contrib', :di_packages, 'all', nil)
- check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :di_packages, 'all', '', updated: false, id_of: component_file_empty_contrib_all_di)
+ check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', '', id_of: component_file_empty_contrib_amd64_di)
check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil)
check_component_file(current_time.round, 'contrib', :sources, nil, nil)
- main_amd64_size = expected_main_amd64_content.length
- main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
+ expected_main_amd64_size = expected_main_amd64_content.length
+ expected_main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
- contrib_all_size = component_file1.size
- contrib_all_sha256 = component_file1.file_sha256
+ expected_main_amd64_di_size = expected_main_amd64_di_content.length
- main_amd64_di_size = expected_main_amd64_di_content.length
- main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content)
-
- main_sources_size = expected_main_sources_content.length
- main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content)
+ expected_main_sources_size = expected_main_sources_content.length
+ expected_main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content)
expected_release_content = <<~EOF
Codename: #{distribution.codename}
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Date: Sat, 25 Jan 2020 15:17:19 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:19 +0000
Acquire-By-Hash: yes
Architectures: all amd64 arm64
Components: contrib main
SHA256:
- #{contrib_all_sha256} #{contrib_all_size.to_s.rjust(8)} contrib/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages
@@ -201,11 +232,11 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages
- #{main_amd64_sha256} #{main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages
- #{main_amd64_di_sha256} #{main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages
+ #{expected_main_amd64_sha256} #{expected_main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages
+ #{expected_main_amd64_di_sha256} #{expected_main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages
- #{main_sources_sha256} #{main_sources_size.to_s.rjust(8)} main/source/Sources
+ #{expected_main_sources_sha256} #{expected_main_sources_size.to_s.rjust(8)} main/source/Sources
EOF
expected_release_content = "Suite: #{distribution.suite}\n#{expected_release_content}" if distribution.suite
@@ -222,7 +253,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
context 'without components and architectures' do
it 'generates minimal distribution', :aggregate_failures do
- travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
+ travel_to(Time.utc(2020, 1, 25, 15, 17, 18, 123456)) do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
expect { subject }
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index 9f940d27341..2070cac24b0 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -63,35 +63,6 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false)
expect(gitlab_shell.repository_exists?('default', old_repository_path)).to be(false)
end
-
- context ':repack_after_shard_migration feature flag disabled' do
- before do
- stub_feature_flags(repack_after_shard_migration: false)
- end
-
- it 'does not enqueue a GC run' do
- expect { subject.execute }
- .not_to change { Projects::GitGarbageCollectWorker.jobs.count }
- end
- end
-
- context ':repack_after_shard_migration feature flag enabled' do
- before do
- stub_feature_flags(repack_after_shard_migration: true)
- end
-
- it 'does not enqueue a GC run if housekeeping is disabled' do
- stub_application_setting(housekeeping_enabled: false)
-
- expect { subject.execute }
- .not_to change { Projects::GitGarbageCollectWorker.jobs.count }
- end
-
- it 'enqueues a GC run' do
- expect { subject.execute }
- .to change { Projects::GitGarbageCollectWorker.jobs.count }.by(1)
- end
- end
end
context 'when the filesystems are the same' do
diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
index 209be09c807..9fcdd296ebe 100644
--- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
@@ -145,7 +145,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
let_it_be(:repository) { project.repository }
it 'is successful' do
- expect(repository).to receive(:root_ref_sha).and_raise(StandardError)
+ expect(repository).to receive(:commit).and_return(nil)
expect(result.status).to eq(:success)
end
end
@@ -168,7 +168,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
it 'returns an error' do
expect { result }.to raise_error { |error|
expect(error).to be_a(Gitlab::Graphql::Errors::MutationError)
- expect(error.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ expect(error.message).to eq('UF: You must <a target="_blank" rel="noopener noreferrer" ' \
'href="http://localhost/help/user/project/repository/index.md' \
'#add-files-to-a-repository">add at least one file to the repository' \
'</a> before using Security features.')
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 e72e8e79411..d3b3434b339 100644
--- a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb
@@ -5,7 +5,6 @@ RSpec.shared_examples 'issue_edit snowplow tracking' do
let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION }
let(:label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL }
let(:namespace) { project.namespace }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
it_behaves_like 'Snowplow event tracking with RedisHLL context'
end
diff --git a/spec/support/stub_dot_com_check.rb b/spec/support/stub_dot_com_check.rb
new file mode 100644
index 00000000000..6934b33d111
--- /dev/null
+++ b/spec/support/stub_dot_com_check.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ %i[saas saas_registration].each do |metadata|
+ config.before(:context, metadata) do
+ # Ensure Gitlab.com? returns true during context.
+ # This is needed for let_it_be which is shared across examples,
+ # therefore the value must be changed in before_all,
+ # but RSpec prevent stubbing method calls in before_all,
+ # therefore we have to resort to temporarily swap url value.
+ @_original_gitlab_url = Gitlab.config.gitlab['url']
+ Gitlab.config.gitlab['url'] = Gitlab::Saas.com_url
+ end
+
+ config.after(:context, metadata) do
+ # Swap back original value
+ Gitlab.config.gitlab['url'] = @_original_gitlab_url
+ end
+ end
+end
diff --git a/spec/support_specs/ability_check_spec.rb b/spec/support_specs/ability_check_spec.rb
new file mode 100644
index 00000000000..ce841112d86
--- /dev/null
+++ b/spec/support_specs/ability_check_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require 'declarative_policy'
+require 'request_store'
+require 'tempfile'
+
+require 'gitlab/safe_request_store'
+
+require_relative '../../app/models/ability'
+require_relative '../support/ability_check'
+
+RSpec.describe Support::AbilityCheck, feature_category: :system_access do # rubocop:disable RSpec/FilePath
+ let(:user) { :user }
+ let(:child) { Testing::Child.new }
+ let(:parent) { Testing::Parent.new(child) }
+
+ before do
+ # Usually done in spec/spec_helper.
+ described_class.inject(Ability.singleton_class)
+
+ stub_const('Testing::BasePolicy', Class.new(DeclarativePolicy::Base))
+
+ stub_const('Testing::Parent', Struct.new(:parent_of))
+ stub_const('Testing::ParentPolicy', Class.new(Testing::BasePolicy) do
+ delegate { @subject.parent_of }
+ condition(:is_adult) { @subject.is_a?(Testing::Parent) }
+ rule { is_adult }.enable :drink_coffee
+ end)
+
+ stub_const('Testing::Child', Class.new)
+ stub_const('Testing::ChildPolicy', Class.new(Testing::BasePolicy) do
+ condition(:always) { true }
+ rule { always }.enable :eat_ice
+ end)
+ end
+
+ def expect_no_deprecation_warning(&block)
+ expect(&block).not_to output.to_stderr
+ end
+
+ def expect_deprecation_warning(policy_class, ability, &block)
+ expect(&block)
+ .to output(/DEPRECATION WARNING: Ability :#{ability} in #{policy_class} not found./)
+ .to_stderr
+ end
+
+ def expect_allowed(user, ability, subject)
+ expect(Ability.allowed?(user, ability, subject))
+ end
+
+ shared_examples 'ability found' do
+ it 'policy ability is found' do
+ expect_no_deprecation_warning do
+ expect_allowed(user, ability, subject).to eq(true)
+ end
+ end
+ end
+
+ shared_examples 'ability not found' do |warning:|
+ description = 'policy ability is not found'
+ description += warning ? ' and emits a warning' : ' without warning'
+
+ it description do
+ check = -> { expect_allowed(user, ability, subject).to eq(false) }
+
+ if warning
+ expect_deprecation_warning(warning, ability, &check)
+ else
+ expect_no_deprecation_warning(&check)
+ end
+ end
+ end
+
+ shared_context 'with custom TODO YAML' do
+ let(:yaml_file) { Tempfile.new }
+
+ before do
+ yaml_file.write(yaml_content)
+ yaml_file.rewind
+
+ stub_const("#{described_class}::Checker::TODO_YAML", yaml_file.path)
+ described_class::Checker.clear_memoization(:todo_list)
+ end
+
+ after do
+ described_class::Checker.clear_memoization(:todo_list)
+ yaml_file.unlink
+ end
+ end
+
+ describe 'checking ability' do
+ context 'with valid direct ability' do
+ let(:subject) { parent }
+ let(:ability) { :drink_coffee }
+
+ include_examples 'ability found'
+
+ context 'with empty TODO yaml' do
+ let(:yaml_content) { nil }
+
+ include_context 'with custom TODO YAML'
+ include_examples 'ability found'
+ end
+
+ context 'with non-Hash TODO yaml' do
+ let(:yaml_content) { '[]' }
+
+ include_context 'with custom TODO YAML'
+ include_examples 'ability found'
+ end
+ end
+
+ context 'with unreachable ability' do
+ let(:subject) { child }
+ let(:ability) { :drink_coffee }
+
+ include_examples 'ability not found', warning: 'Testing::ChildPolicy'
+
+ context 'when ignored in TODO YAML' do
+ let(:yaml_content) do
+ <<~YAML
+ Testing::ChildPolicy:
+ - #{ability}
+ YAML
+ end
+
+ include_context 'with custom TODO YAML'
+ include_examples 'ability not found', warning: false
+ end
+ end
+
+ context 'with unknown ability' do
+ let(:subject) { parent }
+ let(:ability) { :unknown }
+
+ include_examples 'ability not found', warning: 'Testing::ParentPolicy'
+ end
+
+ context 'with delegated ability' do
+ let(:subject) { parent }
+ let(:ability) { :eat_ice }
+
+ include_examples 'ability found'
+ end
+ end
+end
diff --git a/spec/support_specs/helpers/packages/npm_spec.rb b/spec/support_specs/helpers/packages/npm_spec.rb
new file mode 100644
index 00000000000..e1316a10fb1
--- /dev/null
+++ b/spec/support_specs/helpers/packages/npm_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::API::Helpers::Packages::Npm, feature_category: :package_registry do # rubocop: disable RSpec/FilePath
+ let(:object) { klass.new(params) }
+ let(:klass) do
+ Struct.new(:params) do
+ include ::API::Helpers
+ include ::API::Helpers::Packages::Npm
+ end
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:namespace) { group }
+ let_it_be(:project) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package) { create(:npm_package, project: project) }
+
+ describe '#endpoint_scope' do
+ subject { object.endpoint_scope }
+
+ context 'when params includes an id' do
+ let(:params) { { id: 42, package_name: 'foo' } }
+
+ it { is_expected.to eq(:project) }
+ end
+
+ context 'when params does not include an id' do
+ let(:params) { { package_name: 'foo' } }
+
+ it { is_expected.to eq(:instance) }
+ end
+ end
+
+ describe '#finder_for_endpoint_scope' do
+ subject { object.finder_for_endpoint_scope(package_name) }
+
+ let(:package_name) { package.name }
+
+ context 'when called with project scope' do
+ let(:params) { { id: project.id } }
+
+ it 'returns a PackageFinder for project scope' do
+ expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, project: project)
+
+ subject
+ end
+ end
+
+ context 'when called with instance scope' do
+ let(:params) { { package_name: package_name } }
+
+ it 'returns a PackageFinder for namespace scope' do
+ expect(::Packages::Npm::PackageFinder).to receive(:new).with(package_name, namespace: group)
+
+ subject
+ end
+ end
+ end
+
+ describe '#project_id_or_nil' do
+ subject { object.project_id_or_nil }
+
+ context 'when called with project scope' do
+ let(:params) { { id: project.id } }
+
+ it { is_expected.to eq(project.id) }
+ end
+
+ context 'when called with namespace scope' do
+ context 'when given an unscoped name' do
+ let(:params) { { package_name: 'foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that does not match a group name' do
+ let(:params) { { package_name: '@nonexistent-group/foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that matches a group name' do
+ let(:params) { { package_name: package.name } }
+
+ it { is_expected.to eq(project.id) }
+
+ context 'with another package with the same name, in another project in the namespace' do
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
+
+ it 'returns the project id for the newest matching package within the scope' do
+ expect(subject).to eq(project2.id)
+ end
+ end
+ end
+
+ context 'with npm_allow_packages_in_multiple_projects disabled' do
+ before do
+ stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
+ end
+
+ context 'when given an unscoped name' do
+ let(:params) { { package_name: 'foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that does not match a group name' do
+ let(:params) { { package_name: '@nonexistent-group/foo' } }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'when given a scope that matches a group name' do
+ let(:params) { { package_name: package.name } }
+
+ it { is_expected.to eq(project.id) }
+
+ context 'with another package with the same name, in another project in the namespace' do
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
+
+ it 'returns the project id for the newest matching package within the scope' do
+ expect(subject).to eq(project2.id)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support_specs/matchers/exceed_redis_call_limit_spec.rb b/spec/support_specs/matchers/exceed_redis_call_limit_spec.rb
new file mode 100644
index 00000000000..819f50e26b6
--- /dev/null
+++ b/spec/support_specs/matchers/exceed_redis_call_limit_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'RedisCommand matchers', :use_clean_rails_redis_caching, feature_category: :source_code_management do
+ let(:control) do
+ RedisCommands::Recorder.new do
+ Rails.cache.read('test')
+ Rails.cache.read('test')
+ Rails.cache.write('test', 1)
+ end
+ end
+
+ before do
+ Rails.cache.read('warmup')
+ end
+
+ it 'verifies maximum number of Redis calls' do
+ expect(control).not_to exceed_redis_calls_limit(3)
+
+ expect(control).not_to exceed_redis_command_calls_limit(:get, 2)
+ expect(control).not_to exceed_redis_command_calls_limit(:set, 1)
+ end
+
+ it 'verifies minimum number of Redis calls' do
+ expect(control).to exceed_redis_calls_limit(2)
+
+ expect(control).to exceed_redis_command_calls_limit(:get, 1)
+ expect(control).to exceed_redis_command_calls_limit(:set, 0)
+ end
+
+ context 'with Recorder matching only some Redis calls' do
+ it 'counts only Redis calls captured by Recorder' do
+ Rails.cache.write('ignored', 1)
+
+ control = RedisCommands::Recorder.new do
+ Rails.cache.read('recorded')
+ end
+
+ Rails.cache.write('also_ignored', 1)
+
+ expect(control).not_to exceed_redis_calls_limit(1)
+ expect(control).not_to exceed_redis_command_calls_limit(:set, 0)
+ expect(control).not_to exceed_redis_command_calls_limit(:get, 1)
+ end
+ end
+
+ context 'when expect part is a function' do
+ it 'automatically enables RedisCommand::Recorder for it' do
+ func = -> do
+ Rails.cache.read('test')
+ Rails.cache.read('test')
+ end
+
+ expect { func.call }.not_to exceed_redis_calls_limit(2)
+ expect { func.call }.not_to exceed_redis_command_calls_limit(:get, 2)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/db/decomposition/connection_status_spec.rb b/spec/tasks/gitlab/db/decomposition/connection_status_spec.rb
new file mode 100644
index 00000000000..55a50035222
--- /dev/null
+++ b/spec/tasks/gitlab/db/decomposition/connection_status_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:db:decomposition:connection_status', feature_category: :pods do
+ let(:max_connections) { 500 }
+ let(:current_connections) { 300 }
+
+ subject { run_rake_task('gitlab:db:decomposition:connection_status') }
+
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/db/decomposition/connection_status'
+ end
+
+ before do
+ allow(ApplicationRecord.connection).to receive(:select_one).with(any_args).and_return(
+ { "active" => current_connections, "max" => max_connections }
+ )
+ end
+
+ context 'when separate ci database is not configured' do
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ context "when PostgreSQL max_connections is too low" do
+ it 'suggests to increase it' do
+ expect { subject }.to output(
+ "Currently using #{current_connections} connections out of #{max_connections} max_connections,\n" \
+ "which may run out when you switch to two database connections.\n\n" \
+ "Consider increasing PostgreSQL 'max_connections' setting.\n" \
+ "Depending on the installation method, there are different ways to\n" \
+ "increase that setting. Please consult the GitLab documentation.\n"
+ ).to_stdout
+ end
+ end
+
+ context "when PostgreSQL max_connections is high enough" do
+ let(:max_connections) { 1000 }
+
+ it 'only shows current status' do
+ expect { subject }.to output(
+ "Currently using #{current_connections} connections out of #{max_connections} max_connections,\n" \
+ "which is enough for running GitLab using two database connections.\n"
+ ).to_stdout
+ end
+ end
+ end
+
+ context 'when separate ci database is configured' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it "does not show connection information" do
+ expect { subject }.to output(
+ "GitLab database already running on two connections\n"
+ ).to_stdout
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 933eba40719..0f13840ae01 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
before do
# Stub out db tasks
allow(Rake::Task['db:migrate']).to receive(:invoke).and_return(true)
- allow(Rake::Task['db:structure:load']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['db:schema:load']).to receive(:invoke).and_return(true)
allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true)
end
@@ -395,13 +395,19 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
end
- context 'when the dictionary files already exist' do
+ context 'when a new model class is added to the codebase' do
let(:table_class) do
Class.new(ApplicationRecord) do
self.table_name = 'table1'
end
end
+ let(:migration_table_class) do
+ Class.new(Gitlab::Database::Migration[1.0]::MigrationRecord) do
+ self.table_name = 'table1'
+ end
+ end
+
let(:view_class) do
Class.new(ApplicationRecord) do
self.table_name = 'view1'
@@ -410,7 +416,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
table_metadata = {
'table_name' => 'table1',
- 'classes' => [],
+ 'classes' => ['TableClass'],
'feature_categories' => [],
'description' => nil,
'introduced_by_url' => nil,
@@ -418,7 +424,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
}
view_metadata = {
'view_name' => 'view1',
- 'classes' => [],
+ 'classes' => ['ViewClass'],
'feature_categories' => [],
'description' => nil,
'introduced_by_url' => nil,
@@ -426,23 +432,60 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
}
before do
- stub_const('TableClass', table_class)
- stub_const('ViewClass', view_class)
+ stub_const('TableClass1', table_class)
+ stub_const('MIgrationTableClass1', migration_table_class)
+ stub_const('ViewClass1', view_class)
File.write(table_file_path, table_metadata.to_yaml)
File.write(view_file_path, view_metadata.to_yaml)
- allow(model).to receive(:descendants).and_return([table_class, view_class])
+ allow(model).to receive(:descendants).and_return([table_class, migration_table_class, view_class])
end
- it 'update the dictionary content' do
+ it 'appends new classes to the dictionary' do
run_rake_task('gitlab:db:dictionary:generate')
table_metadata = YAML.safe_load(File.read(table_file_path))
- expect(table_metadata['classes']).to match_array(['TableClass'])
+ expect(table_metadata['classes']).to match_array(%w[TableClass TableClass1])
view_metadata = YAML.safe_load(File.read(view_file_path))
- expect(view_metadata['classes']).to match_array(['ViewClass'])
+ expect(view_metadata['classes']).to match_array(%w[ViewClass ViewClass1])
+ end
+ end
+
+ context 'when a model class is removed from the codebase' do
+ table_metadata = {
+ 'table_name' => 'table1',
+ 'classes' => ['TableClass'],
+ 'feature_categories' => [],
+ 'description' => nil,
+ 'introduced_by_url' => nil,
+ 'milestone' => 14.3
+ }
+ view_metadata = {
+ 'view_name' => 'view1',
+ 'classes' => ['ViewClass'],
+ 'feature_categories' => [],
+ 'description' => nil,
+ 'introduced_by_url' => nil,
+ 'milestone' => 14.3
+ }
+
+ before do
+ File.write(table_file_path, table_metadata.to_yaml)
+ File.write(view_file_path, view_metadata.to_yaml)
+
+ allow(model).to receive(:descendants).and_return([])
+ end
+
+ it 'keeps the dictionary classes' do
+ run_rake_task('gitlab:db:dictionary:generate')
+
+ table_metadata = YAML.safe_load(File.read(table_file_path))
+ expect(table_metadata['classes']).to match_array(%w[TableClass])
+
+ view_metadata = YAML.safe_load(File.read(view_file_path))
+ expect(view_metadata['classes']).to match_array(%w[ViewClass])
end
end
end
@@ -805,6 +848,80 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
end
+ describe 'validate_async_constraints' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'delegates ci task to Gitlab::Database::AsyncConstraints' do
+ expect(Gitlab::Database::AsyncConstraints).to receive(:validate_pending_entries!).with(how_many: 2)
+
+ run_rake_task('gitlab:db:validate_async_constraints:ci')
+ end
+
+ it 'delegates ci task to Gitlab::Database::AsyncConstraints with specified argument' do
+ expect(Gitlab::Database::AsyncConstraints).to receive(:validate_pending_entries!).with(how_many: 5)
+
+ run_rake_task('gitlab:db:validate_async_constraints:ci', '[5]')
+ end
+
+ it 'delegates main task to Gitlab::Database::AsyncConstraints' do
+ expect(Gitlab::Database::AsyncConstraints).to receive(:validate_pending_entries!).with(how_many: 2)
+
+ run_rake_task('gitlab:db:validate_async_constraints:main')
+ end
+
+ it 'delegates main task to Gitlab::Database::AsyncConstraints with specified argument' do
+ expect(Gitlab::Database::AsyncConstraints).to receive(:validate_pending_entries!).with(how_many: 7)
+
+ run_rake_task('gitlab:db:validate_async_constraints:main', '[7]')
+ end
+
+ it 'delegates all task to every database with higher default for dev' do
+ expect(Rake::Task['gitlab:db:validate_async_constraints:ci']).to receive(:invoke).with(1000)
+ expect(Rake::Task['gitlab:db:validate_async_constraints:main']).to receive(:invoke).with(1000)
+
+ run_rake_task('gitlab:db:validate_async_constraints:all')
+ end
+
+ it 'delegates all task to every database with lower default for prod' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+
+ expect(Rake::Task['gitlab:db:validate_async_constraints:ci']).to receive(:invoke).with(2)
+ expect(Rake::Task['gitlab:db:validate_async_constraints:main']).to receive(:invoke).with(2)
+
+ run_rake_task('gitlab:db:validate_async_constraints:all')
+ end
+
+ it 'delegates all task to every database with specified argument' do
+ expect(Rake::Task['gitlab:db:validate_async_constraints:ci']).to receive(:invoke).with('50')
+ expect(Rake::Task['gitlab:db:validate_async_constraints:main']).to receive(:invoke).with('50')
+
+ run_rake_task('gitlab:db:validate_async_constraints:all', '[50]')
+ end
+
+ context 'when feature is not enabled' do
+ it 'is a no-op' do
+ stub_feature_flags(database_async_foreign_key_validation: false)
+
+ expect(Gitlab::Database::AsyncConstraints).not_to receive(:validate_pending_entries!)
+
+ expect { run_rake_task('gitlab:db:validate_async_constraints:main') }.to raise_error(SystemExit)
+ end
+ end
+
+ context 'with geo configured' do
+ before do
+ skip_unless_geo_configured
+ end
+
+ it 'does not create a task for the geo database' do
+ expect { run_rake_task('gitlab:db:validate_async_constraints:geo') }
+ .to raise_error(/Don't know how to build task 'gitlab:db:validate_async_constraints:geo'/)
+ end
+ end
+ end
+
describe 'active' do
using RSpec::Parameterized::TableSyntax
@@ -975,14 +1092,6 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
skip_unless_ci_uses_database_tasks
end
- describe 'db:structure:dump against a single database' do
- it 'invokes gitlab:db:clean_structure_sql' do
- expect(Rake::Task['gitlab:db:clean_structure_sql']).to receive(:invoke).twice.and_return(true)
-
- expect { run_rake_task('db:structure:dump:main') }.not_to raise_error
- end
- end
-
describe 'db:schema:dump against a single database' do
it 'invokes gitlab:db:clean_structure_sql' do
expect(Rake::Task['gitlab:db:clean_structure_sql']).to receive(:invoke).once.and_return(true)
diff --git a/spec/tasks/gitlab/feature_categories_rake_spec.rb b/spec/tasks/gitlab/feature_categories_rake_spec.rb
index 22f36309a7c..33f4bca4c85 100644
--- a/spec/tasks/gitlab/feature_categories_rake_spec.rb
+++ b/spec/tasks/gitlab/feature_categories_rake_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
)
),
'api_endpoints' => a_hash_including(
- 'authentication_and_authorization' => a_collection_including(
+ 'system_access' => a_collection_including(
klass: 'API::AccessRequests',
action: '/groups/:id/access_requests',
source_location: [
diff --git a/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb b/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb
index 25ea5d75a56..264dea815f4 100644
--- a/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb
+++ b/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb
@@ -7,7 +7,7 @@ require 'rake_helper'
# is hit in the rake task.
require 'git'
-RSpec.describe 'gitlab:security namespace rake tasks', :silence_stdout, feature_category: :credential_management do
+RSpec.describe 'gitlab:security namespace rake tasks', :silence_stdout, feature_category: :user_management do
let(:fixture_path) { Rails.root.join('spec/fixtures/tasks/gitlab/security') }
let(:output_file) { File.join(__dir__, 'tmp/banned_keys_test.yml') }
let(:git_url) { 'https://github.com/rapid7/ssh-badkeys.git' }
diff --git a/spec/tooling/danger/sidekiq_args_spec.rb b/spec/tooling/danger/sidekiq_args_spec.rb
new file mode 100644
index 00000000000..bfa9ef169de
--- /dev/null
+++ b/spec/tooling/danger/sidekiq_args_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require 'rspec-parameterized'
+require 'gitlab-dangerfiles'
+require 'danger'
+require 'danger/plugins/internal/helper'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/sidekiq_args'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::SidekiqArgs, feature_category: :tooling do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_project_helper) { Tooling::Danger::ProjectHelper }
+
+ subject(:specs) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(specs).to receive(:project_helper).and_return(fake_project_helper)
+ end
+
+ describe '#args_changed?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:before, :after, :result) do
+ " - def perform" | " + def perform(abc)" | true
+ " - def perform" | " + def perform(abc)" | true
+ " - def perform(abc)" | " + def perform(def)" | true
+ " - def perform(abc, def)" | " + def perform(abc)" | true
+ " - def perform(abc, def)" | " + def perform(def, abc)" | true
+ " - def perform" | " - def perform" | false
+ " + def perform" | " + def perform" | false
+ " - def perform(abc)" | " - def perform(abc)" | false
+ " + def perform(abc)" | " + def perform(abc)" | false
+ " - def perform(abc)" | " + def perform_foo(abc)" | false
+ end
+
+ with_them do
+ it 'returns correct result' do
+ expect(specs.args_changed?([before, after])).to eq(result)
+ end
+ end
+ end
+
+ describe '#add_comment_for_matched_line' do
+ let(:filename) { 'app/workers/hello_worker.rb' }
+ let(:file_lines) do
+ [
+ "Module Worker",
+ " def perform",
+ " puts hello world",
+ " end",
+ "end"
+ ]
+ end
+
+ before do
+ allow(specs.project_helper).to receive(:file_lines).and_return(file_lines)
+ end
+
+ context 'when args are changed' do
+ before do
+ allow(specs.helper).to receive(:changed_lines).and_return([" - def perform", " + def perform(abc)"])
+ allow(specs).to receive(:args_changed?).and_return(true)
+ end
+
+ it 'adds suggestion at the correct lines' do
+ expect(specs).to receive(:markdown).with(format(described_class::SUGGEST_MR_COMMENT), file: filename, line: 2)
+
+ specs.add_comment_for_matched_line(filename)
+ end
+ end
+
+ context 'when args are not changed' do
+ before do
+ allow(specs.helper).to receive(:changed_lines).and_return([" - def perform", " - def perform"])
+ allow(specs).to receive(:args_changed?).and_return(false)
+ end
+
+ it 'does not add suggestion' do
+ expect(specs).not_to receive(:markdown)
+
+ specs.add_comment_for_matched_line(filename)
+ end
+ end
+ end
+
+ describe '#changed_worker_files' do
+ let(:base_expected_files) { %w[app/workers/a.rb app/workers/b.rb ee/app/workers/e.rb] }
+
+ before do
+ all_changed_files = %w[
+ app/workers/a.rb
+ app/workers/b.rb
+ ee/app/workers/e.rb
+ spec/foo_spec.rb
+ ee/spec/foo_spec.rb
+ spec/bar_spec.rb
+ ee/spec/bar_spec.rb
+ spec/zab_spec.rb
+ ee/spec/zab_spec.rb
+ ]
+
+ allow(specs.helper).to receive(:all_changed_files).and_return(all_changed_files)
+ end
+
+ it 'returns added, modified, and renamed_after files by default' do
+ expect(specs.changed_worker_files).to match_array(base_expected_files)
+ end
+
+ context 'with include_ee: :exclude' do
+ it 'returns spec files without EE-specific files' do
+ expect(specs.changed_worker_files(ee: :exclude)).not_to include(%w[ee/app/workers/e.rb])
+ end
+ end
+
+ context 'with include_ee: :only' do
+ it 'returns EE-specific spec files only' do
+ expect(specs.changed_worker_files(ee: :only)).to match_array(%w[ee/app/workers/e.rb])
+ end
+ end
+ end
+end
diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb
index cdac5954f92..09550f037d6 100644
--- a/spec/tooling/danger/specs_spec.rb
+++ b/spec/tooling/danger/specs_spec.rb
@@ -259,6 +259,10 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
" ee: true do",
"\n",
"RSpec.describe Issues :aggregate_failures,",
+ " feature_category: :team_planning do",
+ "\n",
+ "RSpec.describe MergeRequest :aggregate_failures,",
+ " :js,",
" feature_category: :team_planning do"
]
end
@@ -275,7 +279,11 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
"+ feature_category: planning_analytics do",
"+RSpec.describe Epics :aggregate_failures,",
"+ ee: true do",
- "+RSpec.describe Issues :aggregate_failures,"
+ "+RSpec.describe Issues :aggregate_failures,",
+ "+RSpec.describe MergeRequest :aggregate_failures,",
+ "+ :js,",
+ "+ feature_category: :team_planning do",
+ "+RSpec.describe 'line in commit diff but no longer in working copy' do"
]
end
diff --git a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
index 72e02547938..12a73480440 100644
--- a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
@@ -129,6 +129,20 @@ RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :to
it 'returns a singularized keyword based on the first folder the file is in' do
expect(subject).to eq(%w[board query])
end
+
+ context 'when the files are under the pages folder' do
+ let(:js_files) do
+ %w[
+ app/assets/javascripts/pages/boards/issue_board_filters.js
+ ee/app/assets/javascripts/pages2/queries/epic_due_date.query.graphql
+ ee/app/assets/javascripts/queries/epic_due_date.query.graphql
+ ]
+ end
+
+ it 'captures the second folder' do
+ expect(subject).to eq(%w[board pages2 query])
+ end
+ end
end
describe '#system_specs_for_edition' do
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index f62ab726631..bd86f1fe08a 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'carrierwave/storage/fog'
-RSpec.describe GitlabUploader do
+RSpec.describe GitlabUploader, feature_category: :shared do
let(:uploader_class) { Class.new(described_class) }
subject(:uploader) { uploader_class.new(double) }
@@ -179,4 +179,19 @@ RSpec.describe GitlabUploader do
it { expect { subject }.to raise_error(RuntimeError, /not supported/) }
end
+
+ describe '.storage_location' do
+ it 'sets the identifier for the storage location options' do
+ uploader_class.storage_location(:artifacts)
+
+ expect(uploader_class.options).to eq(Gitlab.config.artifacts)
+ end
+
+ context 'when given identifier is not known' do
+ it 'raises an error' do
+ expect { uploader_class.storage_location(:foo) }
+ .to raise_error(KeyError)
+ end
+ end
+ end
end
diff --git a/spec/uploaders/object_storage/cdn_spec.rb b/spec/uploaders/object_storage/cdn_spec.rb
index d6c638297fa..0c1966b4df2 100644
--- a/spec/uploaders/object_storage/cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn_spec.rb
@@ -3,19 +3,6 @@
require 'spec_helper'
RSpec.describe ObjectStorage::CDN, feature_category: :build_artifacts do
- let(:cdn_options) do
- {
- 'object_store' => {
- 'cdn' => {
- 'provider' => 'google',
- 'url' => 'https://gitlab.example.com',
- 'key_name' => 'test-key',
- 'key' => Base64.urlsafe_encode64('12345')
- }
- }
- }.freeze
- end
-
let(:uploader_class) do
Class.new(GitlabUploader) do
include ObjectStorage::Concern
@@ -39,44 +26,66 @@ RSpec.describe ObjectStorage::CDN, feature_category: :build_artifacts do
subject { uploader_class.new(object, :file) }
context 'with CDN config' do
+ let(:cdn_options) do
+ {
+ 'object_store' => {
+ 'cdn' => {
+ 'provider' => cdn_provider,
+ 'url' => 'https://gitlab.example.com',
+ 'key_name' => 'test-key',
+ 'key' => Base64.urlsafe_encode64('12345')
+ }
+ }
+ }.freeze
+ end
+
before do
stub_artifacts_object_storage(enabled: true)
- uploader_class.options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
+ options = Settingslogic.new(Gitlab.config.uploads.deep_merge(cdn_options))
+ allow(uploader_class).to receive(:options).and_return(options)
end
- describe '#cdn_enabled_url' do
- it 'calls #cdn_signed_url' do
- expect(subject).not_to receive(:url)
- expect(subject).to receive(:cdn_signed_url).with(query_params).and_call_original
+ context 'with a known CDN provider' do
+ let(:cdn_provider) { 'google' }
- result = subject.cdn_enabled_url(public_ip, query_params)
+ describe '#cdn_enabled_url' do
+ it 'calls #cdn_signed_url' do
+ expect(subject).not_to receive(:url)
+ expect(subject).to receive(:cdn_signed_url).with(query_params).and_call_original
+
+ result = subject.cdn_enabled_url(public_ip, query_params)
- expect(result.used_cdn).to be true
+ expect(result.used_cdn).to be true
+ end
end
- end
- describe '#use_cdn?' do
- it 'returns true' do
- expect(subject.use_cdn?(public_ip)).to be true
+ describe '#use_cdn?' do
+ it 'returns true' do
+ expect(subject.use_cdn?(public_ip)).to be true
+ end
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")
+ 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 'with an unknown CDN provider' do
+ let(:cdn_provider) { 'amazon' }
- expect(subject.cdn_signed_url).to eq("https://cdn.example.com/path")
+ it 'raises an error' do
+ expect { subject.use_cdn?(public_ip) }.to raise_error("Unknown CDN provider: amazon")
end
end
end
context 'without CDN config' do
- before do
- uploader_class.options = Gitlab.config.uploads
- end
-
describe '#cdn_enabled_url' do
it 'calls #url' do
expect(subject).not_to receive(:cdn_signed_url)
@@ -94,15 +103,4 @@ RSpec.describe ObjectStorage::CDN, feature_category: :build_artifacts do
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?(public_ip) }.to raise_error("Unknown CDN provider: amazon")
- end
- end
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 5344dbeb512..0e293ec973c 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -8,7 +8,7 @@ class Implementation < GitlabUploader
include ::RecordsUploads::Concern
prepend ::ObjectStorage::Extension::RecordsUploads
- storage_options Gitlab.config.uploads
+ storage_location :uploads
private
diff --git a/spec/validators/addressable_url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index 9109a899881..c95c0563a55 100644
--- a/spec/validators/addressable_url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -49,10 +49,15 @@ RSpec.describe AddressableUrlValidator do
end
end
- it 'provides all arguments to UrlBlock validate' do
+ it 'provides all arguments to UrlBlocker.validate!' do
+ # AddressableUrlValidator evaluates all procs before passing as arguments.
+ expected_opts = described_class::BLOCKER_VALIDATE_OPTIONS.transform_values do |value|
+ value.is_a?(Proc) ? value.call : value
+ end
+
expect(Gitlab::UrlBlocker)
.to receive(:validate!)
- .with(badge.link_url, described_class::BLOCKER_VALIDATE_OPTIONS)
+ .with(badge.link_url, expected_opts)
.and_return(true)
subject
@@ -302,6 +307,67 @@ RSpec.describe AddressableUrlValidator do
end
end
+ context 'when deny_all_requests_except_allowed is' do
+ let(:url) { 'http://example.com' }
+ let(:options) { { attributes: [:link_url] } }
+ let(:validator) { described_class.new(**options) }
+
+ context 'true' do
+ let(:options) { super().merge(deny_all_requests_except_allowed: true) }
+
+ it 'prevents the url' do
+ badge.link_url = url
+
+ subject
+
+ expect(badge.errors).to be_present
+ end
+ end
+
+ context 'false' do
+ let(:options) { super().merge(deny_all_requests_except_allowed: false) }
+
+ it 'allows the url' do
+ badge.link_url = url
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
+ end
+
+ context 'not given' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:current_application_settings?).and_return(true)
+ stub_application_setting(deny_all_requests_except_allowed: app_setting)
+ end
+
+ context 'when app setting is true' do
+ let(:app_setting) { true }
+
+ it 'prevents the url' do
+ badge.link_url = url
+
+ subject
+
+ expect(badge.errors).to be_present
+ end
+ end
+
+ context 'when app setting is false' do
+ let(:app_setting) { false }
+
+ it 'allows the url' do
+ badge.link_url = url
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
+ end
+ end
+ end
+
context 'when enforce_sanitization is' do
let(:validator) { described_class.new(attributes: [:link_url], enforce_sanitization: enforce_sanitization) }
let(:unsafe_url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
diff --git a/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb b/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb
index 5ef9399487f..d2a30f2c5c0 100644
--- a/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/ci_cd.html.haml_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'admin/application_settings/ci_cd.html.haml' do
allow(view).to receive(:current_user).and_return(user)
end
- describe 'CI CD Runner Registration' do
+ describe 'CI CD Runners' do
it 'has the setting section' do
render
@@ -26,6 +26,9 @@ RSpec.describe 'admin/application_settings/ci_cd.html.haml' do
expect(rendered).to have_content("Runner registration")
expect(rendered).to have_content(s_("Runners|If both settings are disabled, new runners cannot be registered."))
+ expect(rendered).to have_content(
+ s_("Runners|Fetch GitLab Runner release version data from GitLab.com")
+ )
end
end
end
diff --git a/spec/views/admin/application_settings/network.html.haml_spec.rb b/spec/views/admin/application_settings/network.html.haml_spec.rb
new file mode 100644
index 00000000000..3df55be92d5
--- /dev/null
+++ b/spec/views/admin/application_settings/network.html.haml_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'admin/application_settings/network.html.haml', feature_category: :projects do
+ let_it_be(:admin) { build_stubbed(:admin) }
+ let_it_be(:application_setting) { build(:application_setting) }
+
+ before do
+ assign(:application_setting, application_setting)
+ allow(view).to receive(:current_user) { admin }
+ end
+
+ context 'for Projects API rate limit' do
+ it 'renders the `projects_api_rate_limit_unauthenticated` field' do
+ render
+
+ expect(rendered).to have_field('application_setting_projects_api_rate_limit_unauthenticated')
+ end
+
+ context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is turned off' do
+ before do
+ stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
+ end
+
+ it 'does not render the `projects_api_rate_limit_unauthenticated` field' do
+ render
+
+ expect(rendered).not_to have_field('application_setting_projects_api_rate_limit_unauthenticated')
+ end
+ end
+ end
+end
diff --git a/spec/views/admin/sessions/two_factor.html.haml_spec.rb b/spec/views/admin/sessions/two_factor.html.haml_spec.rb
index c7e0edbcd58..6503c08b84c 100644
--- a/spec/views/admin/sessions/two_factor.html.haml_spec.rb
+++ b/spec/views/admin/sessions/two_factor.html.haml_spec.rb
@@ -29,14 +29,10 @@ RSpec.describe 'admin/sessions/two_factor.html.haml' do
end
end
- context 'user has u2f active' do
- let(:user) { create(:admin, :two_factor_via_u2f) }
+ context 'user has WebAuthn active' do
+ let(:user) { create(:admin, :two_factor_via_webauthn) }
- before do
- stub_feature_flags(webauthn: false)
- end
-
- it 'shows enter u2f form' do
+ it 'shows enter WebAuthn form' do
render
expect(rendered).to have_css('#js-login-2fa-device.btn')
diff --git a/spec/views/devise/confirmations/almost_there.html.haml_spec.rb b/spec/views/devise/confirmations/almost_there.html.haml_spec.rb
index c091efe9295..8e12fb5a17e 100644
--- a/spec/views/devise/confirmations/almost_there.html.haml_spec.rb
+++ b/spec/views/devise/confirmations/almost_there.html.haml_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe 'devise/confirmations/almost_there' do
- describe 'confirmations text' do
- subject { render(template: 'devise/confirmations/almost_there') }
+ subject { render(template: 'devise/confirmations/almost_there') }
+ describe 'confirmations text' do
before do
allow(view).to receive(:params).and_return(email: email)
end
@@ -34,4 +34,17 @@ RSpec.describe 'devise/confirmations/almost_there' do
end
end
end
+
+ describe 'register again prompt' do
+ specify do
+ subject
+
+ expect(rendered).to have_content(
+ 'If the email address is incorrect, you can register again with a different email'
+ )
+ expect(rendered).to have_link(
+ 'register again with a different email', href: new_user_registration_path
+ )
+ 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 798c891e75c..bad01ec2c3d 100644
--- a/spec/views/devise/sessions/new.html.haml_spec.rb
+++ b/spec/views/devise/sessions/new.html.haml_spec.rb
@@ -3,14 +3,13 @@
require 'spec_helper'
RSpec.describe 'devise/sessions/new' do
- describe 'marketing text' do
+ describe 'marketing text', :saas do
subject { render(template: 'devise/sessions/new', layout: 'layouts/devise') }
before do
stub_devise
disable_captcha
stub_feature_flags(restyle_login_page: false)
- allow(Gitlab).to receive(:com?).and_return(true)
end
it 'when flash is anything it renders marketing text' do
diff --git a/spec/views/devise/shared/_error_messages.html.haml_spec.rb b/spec/views/devise/shared/_error_messages.html.haml_spec.rb
new file mode 100644
index 00000000000..9f23b049caf
--- /dev/null
+++ b/spec/views/devise/shared/_error_messages.html.haml_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'devise/shared/_error_messages', feature_category: :system_access do
+ describe 'Error messages' do
+ let(:resource) do
+ instance_spy(User, errors: errors, class: User)
+ end
+
+ before do
+ allow(view).to receive(:resource).and_return(resource)
+ end
+
+ context 'with errors', :aggregate_failures do
+ let(:errors) { errors_stub(['Invalid name', 'Invalid password']) }
+
+ it 'shows errors' do
+ render
+
+ expect(rendered).to have_selector('#error_explanation')
+ expect(rendered).to have_content('Invalid name')
+ expect(rendered).to have_content('Invalid password')
+ end
+ end
+
+ context 'without errors' do
+ let(:errors) { [] }
+
+ it 'does not show errors' do
+ render
+
+ expect(rendered).not_to have_selector('#error_explanation')
+ end
+ end
+ end
+
+ def errors_stub(*messages)
+ ActiveModel::Errors.new(double).tap do |errors|
+ messages.each { |msg| errors.add(:base, msg) }
+ end
+ 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 ee9ccbf6ff5..94a5871cb97 100644
--- a/spec/views/devise/shared/_signup_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'devise/shared/_signup_box' do
before do
stub_devise
+ allow(view).to receive(:arkose_labs_enabled?).and_return(false)
allow(view).to receive(:show_omniauth_providers).and_return(false)
allow(view).to receive(:url).and_return('_url_')
allow(view).to receive(:terms_path).and_return(terms_path)
diff --git a/spec/views/events/event/_common.html.haml_spec.rb b/spec/views/events/event/_common.html.haml_spec.rb
index 2160245fb63..cff1ec43a14 100644
--- a/spec/views/events/event/_common.html.haml_spec.rb
+++ b/spec/views/events/event/_common.html.haml_spec.rb
@@ -18,19 +18,6 @@ RSpec.describe 'events/event/_common.html.haml' do
create(:event, :created, project: project, target: work_item, target_type: 'WorkItem', author: user)
end
- context 'when use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- render partial: 'events/event/common', locals: { event: event.present }
- end
-
- it 'renders the correct url' do
- expect(rendered).to have_link(
- work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.id}"
- )
- end
- end
-
it 'renders the correct url with iid' do
expect(rendered).to have_link(
work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.iid}?iid_path=true"
diff --git a/spec/views/groups/group_members/index.html.haml_spec.rb b/spec/views/groups/group_members/index.html.haml_spec.rb
index 0b3b149238f..fdc6b09d32a 100644
--- a/spec/views/groups/group_members/index.html.haml_spec.rb
+++ b/spec/views/groups/group_members/index.html.haml_spec.rb
@@ -25,7 +25,6 @@ RSpec.describe 'groups/group_members/index', :aggregate_failures, feature_catego
expect(rendered).to have_selector('.js-invite-group-trigger')
expect(rendered).to have_selector('.js-invite-members-trigger')
- expect(response).to render_template(partial: 'groups/_invite_members_modal')
end
end
diff --git a/spec/views/layouts/group.html.haml_spec.rb b/spec/views/layouts/group.html.haml_spec.rb
new file mode 100644
index 00000000000..0b8f735a1d6
--- /dev/null
+++ b/spec/views/layouts/group.html.haml_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/group', feature_category: :subgroups do
+ let_it_be(:group) { create(:group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let(:invite_member) { true }
+
+ before do
+ allow(view).to receive(:can_admin_group_member?).and_return(invite_member)
+ assign(:group, group)
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(build_stubbed(:user)))
+ end
+
+ subject do
+ render
+
+ rendered
+ end
+
+ context 'with ability to invite members' do
+ it { is_expected.to have_selector('.js-invite-members-modal') }
+ end
+
+ context 'without ability to invite members' do
+ let(:invite_member) { false }
+
+ it { is_expected.not_to have_selector('.js-invite-members-modal') }
+ 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 178448022d1..2c5882fce3d 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -7,13 +7,13 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
shared_examples_for 'invite member selector' do
context 'with ability to invite members' do
- it { is_expected.to have_link('Invite members', href: href) }
+ it { is_expected.to have_selector('.js-invite-members-trigger') }
end
context 'without ability to invite members' do
let(:invite_member) { false }
- it { is_expected.not_to have_link('Invite members') }
+ it { is_expected.not_to have_selector('.js-invite-members-trigger') }
end
end
@@ -159,6 +159,29 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
expect(rendered).to have_link('New snippet', href: new_snippet_path)
end
+ context 'when partial exists in a menu item' do
+ it 'renders the menu item partial without rendering invite modal partial' do
+ view_model = {
+ title: '_title_',
+ menu_sections: [
+ {
+ title: '_section_title_',
+ menu_items: [
+ ::Gitlab::Nav::TopNavMenuItem
+ .build(id: '_id_', title: '_title_', partial: 'groups/invite_members_top_nav_link')
+ ]
+ }
+ ]
+ }
+
+ allow(view).to receive(:new_dropdown_view_model).and_return(view_model)
+
+ render
+
+ expect(response).to render_template(partial: 'groups/_invite_members_top_nav_link')
+ end
+ end
+
context 'when the user is not allowed to do anything' do
let(:user) { create(:user, :external) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
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 cddff276317..0df490f9b41 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -106,11 +106,11 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
end
end
- describe 'Contributors' do
+ describe 'Contributor statistics' do
it 'has a link to the project contributors path' do
render
- expect(rendered).to have_link('Contributors', href: project_graph_path(project, current_ref, ref_type: 'heads'))
+ expect(rendered).to have_link('Contributor statistics', href: project_graph_path(project, current_ref, ref_type: 'heads'))
end
end
@@ -122,11 +122,11 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
end
end
- describe 'Compare' do
+ describe 'Compare revisions' do
it 'has a link to the project compare path' do
render
- expect(rendered).to have_link('Compare', href: project_compare_index_path(project, from: project.repository.root_ref, to: current_ref))
+ expect(rendered).to have_link('Compare revisions', href: project_compare_index_path(project, from: project.repository.root_ref, to: current_ref))
end
end
end
@@ -310,7 +310,7 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
it 'top level navigation link is not visible' do
render
- expect(rendered).not_to have_link('Security & Compliance')
+ expect(rendered).not_to have_link('Security and Compliance')
end
end
@@ -322,11 +322,11 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
end
it 'top level navigation link is visible' do
- expect(rendered).to have_link('Security & Compliance')
+ expect(rendered).to have_link('Security and Compliance')
end
it 'security configuration link is visible' do
- expect(rendered).to have_link('Configuration', href: project_security_configuration_path(project))
+ expect(rendered).to have_link('Security configuration', href: project_security_configuration_path(project))
end
end
end
diff --git a/spec/views/layouts/project.html.haml_spec.rb b/spec/views/layouts/project.html.haml_spec.rb
new file mode 100644
index 00000000000..588828f7bd6
--- /dev/null
+++ b/spec/views/layouts/project.html.haml_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/project', feature_category: :projects do
+ let(:invite_member) { true }
+
+ before do
+ allow(view).to receive(:can_admin_project_member?).and_return(invite_member)
+ assign(:project, build_stubbed(:project))
+ allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(build_stubbed(:user)))
+ end
+
+ subject do
+ render
+
+ rendered
+ end
+
+ context 'with ability to invite members' do
+ it { is_expected.to have_selector('.js-invite-members-modal') }
+ end
+
+ context 'without ability to invite members' do
+ let(:invite_member) { false }
+
+ it { is_expected.not_to have_selector('.js-invite-members-modal') }
+ 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
index 43dfab87ac9..c3d320a837b 100644
--- a/spec/views/notify/import_issues_csv_email.html.haml_spec.rb
+++ b/spec/views/notify/import_issues_csv_email.html.haml_spec.rb
@@ -5,8 +5,8 @@ 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(:correct_results) { { success: 3, parse_error: false } }
+ let(:errored_results) { { success: 3, error_lines: [5, 6, 7], parse_error: false } }
let(:parse_error_results) { { success: 0, parse_error: true } }
before do
diff --git a/spec/views/notify/import_work_items_csv_email.html.haml_spec.rb b/spec/views/notify/import_work_items_csv_email.html.haml_spec.rb
new file mode 100644
index 00000000000..989481fc2e6
--- /dev/null
+++ b/spec/views/notify/import_work_items_csv_email.html.haml_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'notify/import_work_items_csv_email.html.haml', feature_category: :team_planning do
+ let_it_be(:user) { create(:user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+ let_it_be(:project) { create(:project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
+
+ let(:parse_error) { "Error parsing CSV file. Please make sure it has the correct format" }
+
+ before do
+ assign(:user, user)
+ assign(:project, project)
+ assign(:results, results)
+
+ render
+ end
+
+ shared_examples_for 'no records created' do
+ specify do
+ expect(rendered).to have_content("No work items have been imported.")
+ expect(rendered).not_to have_content("work items successfully imported.")
+ end
+ end
+
+ shared_examples_for 'work item records created' do
+ specify do
+ expect(rendered).not_to have_content("No work items have been imported.")
+ expect(rendered).to have_content("work items successfully imported.")
+ end
+ end
+
+ shared_examples_for 'contains project link' do
+ specify do
+ expect(rendered).to have_link(project.full_name, href: project_url(project))
+ end
+ end
+
+ shared_examples_for 'contains parse error' do
+ specify do
+ expect(rendered).to have_content(parse_error)
+ end
+ end
+
+ shared_examples_for 'does not contain parse error' do
+ specify do
+ expect(rendered).not_to have_content(parse_error)
+ end
+ end
+
+ context 'when no errors found while importing' do
+ let(:results) { { success: 3, parse_error: false } }
+
+ it 'renders correctly' do
+ expect(rendered).not_to have_content("Errors found on line")
+ end
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'work item records created'
+ it_behaves_like 'does not contain parse error'
+ end
+
+ context 'when import errors reported' do
+ let(:results) { { success: 3, error_lines: [5, 6, 7], parse_error: false } }
+
+ it 'renders correctly' do
+ expect(rendered).to have_content("Errors found on lines: #{results[:error_lines].join(', ')}. \
+Please check that these lines have the following fields: title, type")
+ end
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'work item records created'
+ it_behaves_like 'does not contain parse error'
+ end
+
+ context 'when parse error reported while importing' do
+ let(:results) { { success: 0, parse_error: true } }
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'no records created'
+ it_behaves_like 'contains parse error'
+ end
+
+ context 'when work item type column contains blank entries' do
+ let(:results) { { success: 0, parse_error: false, type_errors: { blank: [4] } } }
+
+ it 'renders with missing work item message' do
+ expect(rendered).to have_content("Work item type is empty")
+ end
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'no records created'
+ it_behaves_like 'does not contain parse error'
+ end
+
+ context 'when work item type column contains missing entries' do
+ let(:results) { { success: 0, parse_error: false, type_errors: { missing: [5] } } }
+
+ it 'renders with missing work item message' do
+ expect(rendered).to have_content("Work item type cannot be found or is not supported.")
+ end
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'no records created'
+ it_behaves_like 'does not contain parse error'
+ end
+
+ context 'when work item type column contains disallowed entries' do
+ let(:results) { { success: 0, parse_error: false, type_errors: { disallowed: [6] } } }
+
+ it 'renders with missing work item message' do
+ expect(rendered).to have_content("Work item type is not available.")
+ end
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'no records created'
+ it_behaves_like 'does not contain parse error'
+ end
+
+ context 'when CSV contains multiple kinds of work item type errors' do
+ let(:results) { { success: 0, parse_error: false, type_errors: { blank: [4], missing: [5], disallowed: [6] } } }
+
+ it 'renders with missing work item message' do
+ expect(rendered).to have_content("Work item type is empty")
+ expect(rendered).to have_content("Work item type cannot be found or is not supported.")
+ expect(rendered).to have_content("Work item type is not available. Please check your license and permissions.")
+ end
+
+ it_behaves_like 'contains project link'
+ it_behaves_like 'no records created'
+ it_behaves_like 'does not contain parse error'
+ end
+end
diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb
index 2ddbd3e6e14..d51bfd19c37 100644
--- a/spec/views/profiles/keys/_key.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'profiles/keys/_key.html.haml', feature_category: :authentication_and_authorization do
+RSpec.describe 'profiles/keys/_key.html.haml', feature_category: :system_access do
let_it_be(:user) { create(:user) }
before do
@@ -68,30 +68,21 @@ RSpec.describe 'profiles/keys/_key.html.haml', feature_category: :authentication
end
context 'displays the usage type' do
- where(:usage_type, :usage_type_text, :displayed_buttons, :hidden_buttons, :revoke_ssh_signatures_ff) do
+ where(:usage_type, :usage_type_text, :displayed_buttons, :hidden_buttons) do
[
- [:auth, 'Authentication', ['Remove'], ['Revoke'], true],
- [:auth_and_signing, 'Authentication & Signing', %w[Remove Revoke], [], true],
- [:signing, 'Signing', %w[Remove Revoke], [], true],
- [:auth, 'Authentication', ['Remove'], ['Revoke'], false],
- [:auth_and_signing, 'Authentication & Signing', %w[Remove], ['Revoke'], false],
- [:signing, 'Signing', %w[Remove], ['Revoke'], false]
+ [:auth, 'Authentication', ['Remove'], ['Revoke']],
+ [:auth_and_signing, 'Authentication & Signing', %w[Remove Revoke], []],
+ [:signing, 'Signing', %w[Remove Revoke], []]
]
end
with_them do
let(:key) { create(:key, user: user, usage_type: usage_type) }
- it 'renders usage type text' do
+ it 'renders usage type text and remove/revoke buttons', :aggregate_failures do
render
expect(rendered).to have_text(usage_type_text)
- end
-
- it 'renders remove/revoke buttons', :aggregate_failures do
- stub_feature_flags(revoke_ssh_signatures: revoke_ssh_signatures_ff)
-
- render
displayed_buttons.each do |button|
expect(rendered).to have_text(button)
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index bf154b61609..aeb12abd240 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -101,4 +101,28 @@ RSpec.describe 'projects/edit' do
it_behaves_like 'renders registration features prompt', :project_disabled_repository_size_limit
end
end
+
+ describe 'pages menu entry callout' do
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(show_pages_in_deployments_menu: false)
+ end
+
+ it 'does not show a callout' do
+ render
+ expect(rendered).not_to have_content('GitLab Pages has moved')
+ end
+ end
+
+ context 'with feature flag enabled' do
+ before do
+ stub_feature_flags(show_pages_in_deployments_menu: true)
+ end
+
+ it 'does show a callout' do
+ render
+ expect(rendered).to have_content('GitLab Pages has moved')
+ end
+ end
+ end
end
diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb
index 6077dda3c98..2b19b364365 100644
--- a/spec/views/projects/empty.html.haml_spec.rb
+++ b/spec/views/projects/empty.html.haml_spec.rb
@@ -73,9 +73,6 @@ RSpec.describe 'projects/empty' do
expect(rendered).to have_content('Invite your team')
expect(rendered).to have_content('Add members to this project and start collaborating with your team.')
expect(rendered).to have_selector('.js-invite-members-trigger')
- expect(rendered).to have_selector('.js-invite-members-modal')
- expect(rendered).to have_selector('[data-label=invite_members_empty_project]')
- expect(rendered).to have_selector('[data-event=click_button]')
expect(rendered).to have_selector('[data-trigger-source=project-empty-page]')
end
@@ -87,7 +84,6 @@ RSpec.describe 'projects/empty' do
expect(rendered).not_to have_content('Invite your team')
expect(rendered).not_to have_selector('.js-invite-members-trigger')
- expect(rendered).not_to have_selector('.js-invite-members-modal')
end
end
end
diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
index 37c9908af1d..13ec7207ec9 100644
--- a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
+++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'projects/pipeline_schedules/_pipeline_schedule' do
let(:user) { maintainer }
before do
- allow(view).to receive(:can?).with(maintainer, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true)
+ allow(view).to receive(:can?).with(maintainer, :admin_pipeline_schedule, pipeline_schedule).and_return(true)
end
it 'non-owner can take ownership of pipeline' do
@@ -36,7 +36,7 @@ RSpec.describe 'projects/pipeline_schedules/_pipeline_schedule' do
let(:user) { owner }
before do
- allow(view).to receive(:can?).with(owner, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(false)
+ allow(view).to receive(:can?).with(owner, :admin_pipeline_schedule, pipeline_schedule).and_return(false)
end
it 'owner cannot take ownership of pipeline' do
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index b9c7da20d1a..81a11874886 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_authoring do
+RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_composition do
include Devise::Test::ControllerHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -13,6 +13,7 @@ RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_authoring
before do
assign(:project, project)
assign(:pipeline, presented_pipeline)
+ allow(view).to receive(:current_user) { user }
end
context 'when pipeline has errors' do
@@ -32,6 +33,22 @@ RSpec.describe 'projects/pipelines/show', feature_category: :pipeline_authoring
expect(rendered).not_to have_selector('#js-pipeline-tabs')
end
+
+ it 'renders the pipeline editor button with correct link for users who can view' do
+ project.add_developer(user)
+
+ render
+
+ expect(rendered).to have_link s_('Go to the pipeline editor'),
+ href: project_ci_pipeline_editor_path(project)
+ end
+
+ it 'renders the pipeline editor button with correct link for users who can not view' do
+ render
+
+ expect(rendered).not_to have_link s_('Go to the pipeline editor'),
+ href: project_ci_pipeline_editor_path(project)
+ end
end
context 'when pipeline is valid' do
diff --git a/spec/views/projects/project_members/index.html.haml_spec.rb b/spec/views/projects/project_members/index.html.haml_spec.rb
index 4c4cde01cca..2fcc5c6935b 100644
--- a/spec/views/projects/project_members/index.html.haml_spec.rb
+++ b/spec/views/projects/project_members/index.html.haml_spec.rb
@@ -28,7 +28,6 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures, feature_ca
expect(rendered).to have_selector('.js-invite-group-trigger')
expect(rendered).to have_selector('.js-invite-members-trigger')
expect(rendered).not_to have_content('Members can be added by project')
- expect(response).to render_template(partial: 'projects/_invite_members_modal')
end
context 'when project is not allowed to share with group' do
diff --git a/spec/workers/admin_email_worker_spec.rb b/spec/workers/admin_email_worker_spec.rb
index 1a5cb90bc17..bedf8f0362f 100644
--- a/spec/workers/admin_email_worker_spec.rb
+++ b/spec/workers/admin_email_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AdminEmailWorker do
+RSpec.describe AdminEmailWorker, feature_category: :source_code_management do
subject(:worker) { described_class.new }
describe '.perform' do
diff --git a/spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb b/spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb
index 735e4a214a9..cdb7357c184 100644
--- a/spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb
+++ b/spec/workers/analytics/usage_trends/count_job_trigger_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Analytics::UsageTrends::CountJobTriggerWorker do
+RSpec.describe Analytics::UsageTrends::CountJobTriggerWorker, feature_category: :devops_reports do
it_behaves_like 'an idempotent worker'
context 'triggers a job for each measurement identifiers' do
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 ee1bbafa9b5..4155e3522a7 100644
--- a/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
+++ b/spec/workers/analytics/usage_trends/counter_job_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Analytics::UsageTrends::CounterJobWorker do
+RSpec.describe Analytics::UsageTrends::CounterJobWorker, feature_category: :devops_reports do
let_it_be(:user_1) { create(:user) }
let_it_be(:user_2) { create(:user) }
diff --git a/spec/workers/approve_blocked_pending_approval_users_worker_spec.rb b/spec/workers/approve_blocked_pending_approval_users_worker_spec.rb
index bd603bd870d..ffcc58132db 100644
--- a/spec/workers/approve_blocked_pending_approval_users_worker_spec.rb
+++ b/spec/workers/approve_blocked_pending_approval_users_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ApproveBlockedPendingApprovalUsersWorker, type: :worker do
+RSpec.describe ApproveBlockedPendingApprovalUsersWorker, type: :worker, feature_category: :user_profile do
let_it_be(:admin) { create(:admin) }
let_it_be(:active_user) { create(:user) }
let_it_be(:blocked_user) { create(:user, state: 'blocked_pending_approval') }
diff --git a/spec/workers/authorized_keys_worker_spec.rb b/spec/workers/authorized_keys_worker_spec.rb
index 50236f9ea7b..9fab6910441 100644
--- a/spec/workers/authorized_keys_worker_spec.rb
+++ b/spec/workers/authorized_keys_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedKeysWorker do
+RSpec.describe AuthorizedKeysWorker, feature_category: :source_code_management do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb b/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb
index 9d4d48d0568..77c56497ef0 100644
--- a/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb
+++ b/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::PeriodicRecalculateWorker do
+RSpec.describe AuthorizedProjectUpdate::PeriodicRecalculateWorker, feature_category: :source_code_management do
describe '#perform' do
it 'calls AuthorizedProjectUpdate::PeriodicRecalculateService' do
expect_next_instance_of(AuthorizedProjectUpdate::PeriodicRecalculateService) do |service|
diff --git a/spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb b/spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb
index 57a0726000f..5dcb4a67ae4 100644
--- a/spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb
+++ b/spec/workers/authorized_project_update/project_recalculate_per_user_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker do
+RSpec.describe AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker, feature_category: :system_access do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb b/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb
index a9a15565580..7c9d2891b01 100644
--- a/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb
+++ b/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateWorker do
+RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateWorker, feature_category: :system_access do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb
index da4b726c0b5..e6a312d34ce 100644
--- a/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb
+++ b/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::UserRefreshFromReplicaWorker do
+RSpec.describe AuthorizedProjectUpdate::UserRefreshFromReplicaWorker, feature_category: :system_access do
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.namespace.owner }
diff --git a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
index 7c0c4d5bab4..081bece09e9 100644
--- a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
+++ b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do
+RSpec.describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker, feature_category: :system_access do
let_it_be(:project) { create(:project) }
let(:user) { project.namespace.owner }
diff --git a/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
index bd16eeb4712..ef6c3dd43c8 100644
--- a/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
+++ b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker do
+RSpec.describe AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker, feature_category: :system_access do
it 'is labeled as low urgency' do
expect(described_class.get_urgency).to eq(:low)
end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index fbfde77be97..ea009f06a28 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe AuthorizedProjectsWorker do
+RSpec.describe AuthorizedProjectsWorker, feature_category: :system_access do
it_behaves_like "refreshes user's project authorizations"
end
diff --git a/spec/workers/auto_devops/disable_worker_spec.rb b/spec/workers/auto_devops/disable_worker_spec.rb
index e1de97e0ce5..8f7f305b186 100644
--- a/spec/workers/auto_devops/disable_worker_spec.rb
+++ b/spec/workers/auto_devops/disable_worker_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe AutoDevops::DisableWorker, '#perform' do
+RSpec.describe AutoDevops::DisableWorker, '#perform', feature_category: :auto_devops do
let(:user) { create(:user, developer_projects: [project]) }
let(:project) { create(:project, :repository, :auto_devops) }
let(:auto_devops) { project.auto_devops }
diff --git a/spec/workers/auto_merge_process_worker_spec.rb b/spec/workers/auto_merge_process_worker_spec.rb
index 00d27d9c2b5..550590ff6a3 100644
--- a/spec/workers/auto_merge_process_worker_spec.rb
+++ b/spec/workers/auto_merge_process_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AutoMergeProcessWorker do
+RSpec.describe AutoMergeProcessWorker, feature_category: :continuous_delivery do
describe '#perform' do
subject { described_class.new.perform(merge_request&.id) }
diff --git a/spec/workers/background_migration/ci_database_worker_spec.rb b/spec/workers/background_migration/ci_database_worker_spec.rb
index 82c562c4042..1048a06bb12 100644
--- a/spec/workers/background_migration/ci_database_worker_spec.rb
+++ b/spec/workers/background_migration/ci_database_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe BackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state, if: Gitlab::Database.has_config?(:ci) do
+RSpec.describe BackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state, if: Gitlab::Database.has_config?(:ci), feature_category: :database do
it_behaves_like 'it runs background migration jobs', 'ci'
end
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
index 1558c3c9250..32ee6708736 100644
--- a/spec/workers/background_migration_worker_spec.rb
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe BackgroundMigrationWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe BackgroundMigrationWorker, :clean_gitlab_redis_shared_state, feature_category: :database do
it_behaves_like 'it runs background migration jobs', 'main'
end
diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb
index 80dc36d268f..f8efc9c455d 100644
--- a/spec/workers/build_hooks_worker_spec.rb
+++ b/spec/workers/build_hooks_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BuildHooksWorker do
+RSpec.describe BuildHooksWorker, feature_category: :continuous_integration do
describe '#perform' do
context 'when build exists' do
let!(:build) { create(:ci_build) }
diff --git a/spec/workers/build_queue_worker_spec.rb b/spec/workers/build_queue_worker_spec.rb
index 0786722e647..1f3640e7496 100644
--- a/spec/workers/build_queue_worker_spec.rb
+++ b/spec/workers/build_queue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BuildQueueWorker do
+RSpec.describe BuildQueueWorker, feature_category: :continuous_integration do
describe '#perform' do
context 'when build exists' do
let!(:build) { create(:ci_build) }
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
index 3241c931dc5..be9802eb2ce 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/build_success_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BuildSuccessWorker do
+RSpec.describe BuildSuccessWorker, feature_category: :continuous_integration do
describe '#perform' do
subject { described_class.new.perform(build.id) }
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index 4cd37c93d5f..dada4ef63b3 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::EntityWorker do
+RSpec.describe BulkImports::EntityWorker, feature_category: :importers do
let_it_be(:entity) { create(:bulk_import_entity) }
let_it_be(:pipeline_tracker) do
diff --git a/spec/workers/bulk_imports/relation_export_worker_spec.rb b/spec/workers/bulk_imports/relation_export_worker_spec.rb
index 63f1992d186..c2f7831896b 100644
--- a/spec/workers/bulk_imports/relation_export_worker_spec.rb
+++ b/spec/workers/bulk_imports/relation_export_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::RelationExportWorker do
+RSpec.describe BulkImports::RelationExportWorker, feature_category: :importers do
let_it_be(:jid) { 'jid' }
let_it_be(:relation) { 'labels' }
let_it_be(:user) { create(:user) }
diff --git a/spec/workers/bulk_imports/stuck_import_worker_spec.rb b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
index 7dfb6532c07..ba1b1b66b00 100644
--- a/spec/workers/bulk_imports/stuck_import_worker_spec.rb
+++ b/spec/workers/bulk_imports/stuck_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::StuckImportWorker do
+RSpec.describe BulkImports::StuckImportWorker, feature_category: :importers do
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
diff --git a/spec/workers/chat_notification_worker_spec.rb b/spec/workers/chat_notification_worker_spec.rb
index a20a136d197..a9413a94e4b 100644
--- a/spec/workers/chat_notification_worker_spec.rb
+++ b/spec/workers/chat_notification_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatNotificationWorker do
+RSpec.describe ChatNotificationWorker, feature_category: :integrations do
let(:worker) { described_class.new }
let(:chat_build) do
create(:ci_build, pipeline: create(:ci_pipeline, source: :chat))
diff --git a/spec/workers/ci/archive_trace_worker_spec.rb b/spec/workers/ci/archive_trace_worker_spec.rb
index 3ac769aab9e..056093f01b4 100644
--- a/spec/workers/ci/archive_trace_worker_spec.rb
+++ b/spec/workers/ci/archive_trace_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ArchiveTraceWorker do
+RSpec.describe Ci::ArchiveTraceWorker, feature_category: :continuous_integration do
describe '#perform' do
subject { described_class.new.perform(job&.id) }
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 0c1010960a1..1eb88258c62 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -42,20 +42,6 @@ RSpec.describe Ci::ArchiveTracesCronWorker, feature_category: :continuous_integr
subject
end
- context "with FF deduplicate_archive_traces_cron_worker false" do
- before do
- stub_feature_flags(deduplicate_archive_traces_cron_worker: false)
- end
-
- it 'calls execute service' do
- expect_next_instance_of(Ci::ArchiveTraceService) do |instance|
- expect(instance).to receive(:execute).with(build, worker_name: "Ci::ArchiveTracesCronWorker")
- end
-
- subject
- end
- end
-
context 'when the job finished recently' do
let(:finished_at) { 1.hour.ago }
diff --git a/spec/workers/ci/build_finished_worker_spec.rb b/spec/workers/ci/build_finished_worker_spec.rb
index 049f3af1dd7..6da30a86b54 100644
--- a/spec/workers/ci/build_finished_worker_spec.rb
+++ b/spec/workers/ci/build_finished_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildFinishedWorker do
+RSpec.describe Ci::BuildFinishedWorker, feature_category: :continuous_integration do
include AfterNextHelpers
subject { described_class.new.perform(build.id) }
diff --git a/spec/workers/ci/build_prepare_worker_spec.rb b/spec/workers/ci/build_prepare_worker_spec.rb
index b2c74a920ea..d3d607d8f39 100644
--- a/spec/workers/ci/build_prepare_worker_spec.rb
+++ b/spec/workers/ci/build_prepare_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildPrepareWorker do
+RSpec.describe Ci::BuildPrepareWorker, feature_category: :continuous_integration do
subject { described_class.new.perform(build_id) }
context 'build exists' do
diff --git a/spec/workers/ci/build_schedule_worker_spec.rb b/spec/workers/ci/build_schedule_worker_spec.rb
index f8b4efc562b..f0d43ef810d 100644
--- a/spec/workers/ci/build_schedule_worker_spec.rb
+++ b/spec/workers/ci/build_schedule_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildScheduleWorker do
+RSpec.describe Ci::BuildScheduleWorker, feature_category: :continuous_integration do
subject { described_class.new.perform(build.id) }
context 'when build is found' do
diff --git a/spec/workers/ci/build_trace_chunk_flush_worker_spec.rb b/spec/workers/ci/build_trace_chunk_flush_worker_spec.rb
index 8aac80a02be..851b8f6bc90 100644
--- a/spec/workers/ci/build_trace_chunk_flush_worker_spec.rb
+++ b/spec/workers/ci/build_trace_chunk_flush_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::BuildTraceChunkFlushWorker do
+RSpec.describe Ci::BuildTraceChunkFlushWorker, feature_category: :continuous_integration do
let(:data) { 'x' * Ci::BuildTraceChunk::CHUNK_SIZE }
let(:chunk) do
diff --git a/spec/workers/ci/cancel_pipeline_worker_spec.rb b/spec/workers/ci/cancel_pipeline_worker_spec.rb
index 6165aaff1c7..874273a39e1 100644
--- a/spec/workers/ci/cancel_pipeline_worker_spec.rb
+++ b/spec/workers/ci/cancel_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures do
+RSpec.describe Ci::CancelPipelineWorker, :aggregate_failures, feature_category: :continuous_integration do
let!(:pipeline) { create(:ci_pipeline, :running) }
describe '#perform' do
diff --git a/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb b/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb
index 372b0de1b54..6f4bfe897a8 100644
--- a/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb
+++ b/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreateCrossProjectPipelineWorker do
+RSpec.describe Ci::CreateCrossProjectPipelineWorker, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/workers/ci/create_downstream_pipeline_worker_spec.rb b/spec/workers/ci/create_downstream_pipeline_worker_spec.rb
index b4add681e67..bb763e68504 100644
--- a/spec/workers/ci/create_downstream_pipeline_worker_spec.rb
+++ b/spec/workers/ci/create_downstream_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::CreateDownstreamPipelineWorker do
+RSpec.describe Ci::CreateDownstreamPipelineWorker, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/workers/ci/daily_build_group_report_results_worker_spec.rb b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
index e13c6311e46..3d86ff6999b 100644
--- a/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
+++ b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DailyBuildGroupReportResultsWorker do
+RSpec.describe Ci::DailyBuildGroupReportResultsWorker, feature_category: :code_testing do
describe '#perform' do
let!(:pipeline) { create(:ci_pipeline) }
diff --git a/spec/workers/ci/delete_objects_worker_spec.rb b/spec/workers/ci/delete_objects_worker_spec.rb
index 3d985dffdc5..808ad95531e 100644
--- a/spec/workers/ci/delete_objects_worker_spec.rb
+++ b/spec/workers/ci/delete_objects_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DeleteObjectsWorker do
+RSpec.describe Ci::DeleteObjectsWorker, feature_category: :continuous_integration do
let(:worker) { described_class.new }
it { expect(described_class.idempotent?).to be_truthy }
diff --git a/spec/workers/ci/delete_unit_tests_worker_spec.rb b/spec/workers/ci/delete_unit_tests_worker_spec.rb
index ff2575b19c1..ae8fe762334 100644
--- a/spec/workers/ci/delete_unit_tests_worker_spec.rb
+++ b/spec/workers/ci/delete_unit_tests_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DeleteUnitTestsWorker do
+RSpec.describe Ci::DeleteUnitTestsWorker, feature_category: :code_testing do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/ci/drop_pipeline_worker_spec.rb b/spec/workers/ci/drop_pipeline_worker_spec.rb
index 5e626112520..23ae95ee53a 100644
--- a/spec/workers/ci/drop_pipeline_worker_spec.rb
+++ b/spec/workers/ci/drop_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::DropPipelineWorker do
+RSpec.describe Ci::DropPipelineWorker, feature_category: :continuous_integration do
include AfterNextHelpers
let(:pipeline) { create(:ci_pipeline, :running) }
diff --git a/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb b/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb
index 0460738f3f2..9d4e5380474 100644
--- a/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb
+++ b/spec/workers/ci/job_artifacts/expire_project_build_artifacts_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker do
+RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker, feature_category: :build_artifacts do
let(:worker) { described_class.new }
let(:current_time) { Time.current }
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
index 0d4b8243050..bbc2bcf9ac9 100644
--- a/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
+++ b/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::TrackArtifactReportWorker do
+RSpec.describe Ci::JobArtifacts::TrackArtifactReportWorker, feature_category: :code_testing do
describe '#perform', :clean_gitlab_redis_shared_state do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb b/spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb
index e5de0ba0143..ec12ee845a4 100644
--- a/spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb
+++ b/spec/workers/ci/merge_requests/add_todo_when_build_fails_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::MergeRequests::AddTodoWhenBuildFailsWorker do
+RSpec.describe Ci::MergeRequests::AddTodoWhenBuildFailsWorker, feature_category: :code_review_workflow do
describe '#perform' do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline) }
diff --git a/spec/workers/ci/parse_secure_file_metadata_worker_spec.rb b/spec/workers/ci/parse_secure_file_metadata_worker_spec.rb
index 57bbd8a6ff0..11a01352fcc 100644
--- a/spec/workers/ci/parse_secure_file_metadata_worker_spec.rb
+++ b/spec/workers/ci/parse_secure_file_metadata_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ParseSecureFileMetadataWorker do
+RSpec.describe Ci::ParseSecureFileMetadataWorker, feature_category: :mobile_devops do
describe '#perform' do
include_examples 'an idempotent worker' do
let(:secure_file) { create(:ci_secure_file) }
diff --git a/spec/workers/ci/pending_builds/update_group_worker_spec.rb b/spec/workers/ci/pending_builds/update_group_worker_spec.rb
index 8c6bf018158..c16262c0502 100644
--- a/spec/workers/ci/pending_builds/update_group_worker_spec.rb
+++ b/spec/workers/ci/pending_builds/update_group_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PendingBuilds::UpdateGroupWorker do
+RSpec.describe Ci::PendingBuilds::UpdateGroupWorker, feature_category: :subgroups do
describe '#perform' do
let(:worker) { described_class.new }
diff --git a/spec/workers/ci/pending_builds/update_project_worker_spec.rb b/spec/workers/ci/pending_builds/update_project_worker_spec.rb
index 4a67127564e..281b4fb920b 100644
--- a/spec/workers/ci/pending_builds/update_project_worker_spec.rb
+++ b/spec/workers/ci/pending_builds/update_project_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PendingBuilds::UpdateProjectWorker do
+RSpec.describe Ci::PendingBuilds::UpdateProjectWorker, feature_category: :projects do
describe '#perform' do
let(:worker) { described_class.new }
diff --git a/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb
index 7b28384a5bf..b594f661a9a 100644
--- a/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb
+++ b/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineArtifacts::CoverageReportWorker do
+RSpec.describe Ci::PipelineArtifacts::CoverageReportWorker, feature_category: :code_testing do
describe '#perform' do
let(:pipeline_id) { pipeline.id }
diff --git a/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
index 5096691270a..2ad1609c7c4 100644
--- a/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
+++ b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::PipelineArtifacts::CreateQualityReportWorker do
+RSpec.describe ::Ci::PipelineArtifacts::CreateQualityReportWorker, feature_category: :code_quality do
describe '#perform' do
subject { described_class.new.perform(pipeline_id) }
diff --git a/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb
index 274f848ad88..ca4fcc564fa 100644
--- a/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb
+++ b/spec/workers/ci/pipeline_artifacts/expire_artifacts_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineArtifacts::ExpireArtifactsWorker do
+RSpec.describe Ci::PipelineArtifacts::ExpireArtifactsWorker, feature_category: :build_artifacts do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/ci/pipeline_bridge_status_worker_spec.rb b/spec/workers/ci/pipeline_bridge_status_worker_spec.rb
index 6ec5eb0e639..4662b25a3cb 100644
--- a/spec/workers/ci/pipeline_bridge_status_worker_spec.rb
+++ b/spec/workers/ci/pipeline_bridge_status_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineBridgeStatusWorker do
+RSpec.describe Ci::PipelineBridgeStatusWorker, feature_category: :continuous_integration do
describe '#perform' do
subject { described_class.new.perform(pipeline_id) }
diff --git a/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb b/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
index 3b33972c76f..70821f3a833 100644
--- a/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
+++ b/spec/workers/ci/pipeline_success_unlock_artifacts_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineSuccessUnlockArtifactsWorker do
+RSpec.describe Ci::PipelineSuccessUnlockArtifactsWorker, feature_category: :build_artifacts do
describe '#perform' do
subject(:perform) { described_class.new.perform(pipeline_id) }
diff --git a/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb b/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
index f14b7f9d1d0..ede4dad1272 100644
--- a/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
+++ b/spec/workers/ci/ref_delete_unlock_artifacts_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RefDeleteUnlockArtifactsWorker do
+RSpec.describe Ci::RefDeleteUnlockArtifactsWorker, feature_category: :build_artifacts do
describe '#perform' do
subject(:perform) { worker.perform(project_id, user_id, ref) }
diff --git a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb
index 785cba24f9d..e3e7047db56 100644
--- a/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb
+++ b/spec/workers/ci/resource_groups/assign_resource_from_resource_group_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do
+RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker, feature_category: :continuous_delivery do
let(:worker) { described_class.new }
it 'has the `until_executed` deduplicate strategy' do
diff --git a/spec/workers/ci/retry_pipeline_worker_spec.rb b/spec/workers/ci/retry_pipeline_worker_spec.rb
index c7600a24280..f41b6b88c6f 100644
--- a/spec/workers/ci/retry_pipeline_worker_spec.rb
+++ b/spec/workers/ci/retry_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RetryPipelineWorker do
+RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integration do
describe '#perform' do
subject(:perform) { described_class.new.perform(pipeline_id, user_id) }
diff --git a/spec/workers/ci/schedule_delete_objects_cron_worker_spec.rb b/spec/workers/ci/schedule_delete_objects_cron_worker_spec.rb
index 142df271f90..cab282df5f2 100644
--- a/spec/workers/ci/schedule_delete_objects_cron_worker_spec.rb
+++ b/spec/workers/ci/schedule_delete_objects_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ScheduleDeleteObjectsCronWorker do
+RSpec.describe Ci::ScheduleDeleteObjectsCronWorker, feature_category: :continuous_integration do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/ci/stuck_builds/drop_running_worker_spec.rb b/spec/workers/ci/stuck_builds/drop_running_worker_spec.rb
index 6d3aa71fe81..6823686997f 100644
--- a/spec/workers/ci/stuck_builds/drop_running_worker_spec.rb
+++ b/spec/workers/ci/stuck_builds/drop_running_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::StuckBuilds::DropRunningWorker do
+RSpec.describe Ci::StuckBuilds::DropRunningWorker, feature_category: :continuous_integration do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb b/spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb
index 57be799d890..58b07d11d33 100644
--- a/spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb
+++ b/spec/workers/ci/stuck_builds/drop_scheduled_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::StuckBuilds::DropScheduledWorker do
+RSpec.describe Ci::StuckBuilds::DropScheduledWorker, feature_category: :continuous_integration do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/ci/test_failure_history_worker_spec.rb b/spec/workers/ci/test_failure_history_worker_spec.rb
index 7530077d4ad..bf8ec44ce4d 100644
--- a/spec/workers/ci/test_failure_history_worker_spec.rb
+++ b/spec/workers/ci/test_failure_history_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::TestFailureHistoryWorker do
+RSpec.describe ::Ci::TestFailureHistoryWorker, feature_category: :static_application_security_testing do
describe '#perform' do
subject(:perform) { described_class.new.perform(pipeline_id) }
diff --git a/spec/workers/ci/track_failed_build_worker_spec.rb b/spec/workers/ci/track_failed_build_worker_spec.rb
index 12d0e64afc5..5d12e86d844 100644
--- a/spec/workers/ci/track_failed_build_worker_spec.rb
+++ b/spec/workers/ci/track_failed_build_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Ci::TrackFailedBuildWorker do
+RSpec.describe ::Ci::TrackFailedBuildWorker, feature_category: :static_application_security_testing do
let_it_be(:build) { create(:ci_build, :failed, :sast_report) }
let_it_be(:exit_code) { 42 }
let_it_be(:failure_reason) { "script_failure" }
diff --git a/spec/workers/ci/update_locked_unknown_artifacts_worker_spec.rb b/spec/workers/ci/update_locked_unknown_artifacts_worker_spec.rb
index b42d135b1b6..4bb1d3561f9 100644
--- a/spec/workers/ci/update_locked_unknown_artifacts_worker_spec.rb
+++ b/spec/workers/ci/update_locked_unknown_artifacts_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UpdateLockedUnknownArtifactsWorker do
+RSpec.describe Ci::UpdateLockedUnknownArtifactsWorker, feature_category: :build_artifacts do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/ci_platform_metrics_update_cron_worker_spec.rb b/spec/workers/ci_platform_metrics_update_cron_worker_spec.rb
index 0bb6822a0a5..ac00956e1c0 100644
--- a/spec/workers/ci_platform_metrics_update_cron_worker_spec.rb
+++ b/spec/workers/ci_platform_metrics_update_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CiPlatformMetricsUpdateCronWorker, type: :worker do
+RSpec.describe CiPlatformMetricsUpdateCronWorker, type: :worker, feature_category: :continuous_integration do
describe '#perform' do
subject { described_class.new.perform }
diff --git a/spec/workers/cleanup_container_repository_worker_spec.rb b/spec/workers/cleanup_container_repository_worker_spec.rb
index 817b71c8cc6..c970c9ef842 100644
--- a/spec/workers/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/cleanup_container_repository_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_state, feature_category: :container_registry do
let(:repository) { create(:container_repository) }
let(:project) { repository.project }
let(:user) { project.first_owner }
diff --git a/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb b/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb
index 1a5ca744091..b439df4e119 100644
--- a/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb
+++ b/spec/workers/clusters/agents/delete_expired_events_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::DeleteExpiredEventsWorker do
+RSpec.describe Clusters::Agents::DeleteExpiredEventsWorker, feature_category: :kubernetes_management do
let(:agent) { create(:cluster_agent) }
describe '#perform' do
diff --git a/spec/workers/clusters/applications/activate_integration_worker_spec.rb b/spec/workers/clusters/applications/activate_integration_worker_spec.rb
index 5163e4681fa..40a774e1818 100644
--- a/spec/workers/clusters/applications/activate_integration_worker_spec.rb
+++ b/spec/workers/clusters/applications/activate_integration_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Applications::ActivateIntegrationWorker, '#perform' do
+RSpec.describe Clusters::Applications::ActivateIntegrationWorker, '#perform', feature_category: :kubernetes_management do
context 'when cluster exists' do
describe 'prometheus integration' do
let(:integration_name) { 'prometheus' }
diff --git a/spec/workers/clusters/applications/deactivate_integration_worker_spec.rb b/spec/workers/clusters/applications/deactivate_integration_worker_spec.rb
index 62792a3b7d9..f02ad18c7cc 100644
--- a/spec/workers/clusters/applications/deactivate_integration_worker_spec.rb
+++ b/spec/workers/clusters/applications/deactivate_integration_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Applications::DeactivateIntegrationWorker, '#perform' do
+RSpec.describe Clusters::Applications::DeactivateIntegrationWorker, '#perform', feature_category: :kubernetes_management do
context 'when cluster exists' do
describe 'prometheus integration' do
let(:integration_name) { 'prometheus' }
diff --git a/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb b/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb
index c24ca71eb35..15fc9e8678e 100644
--- a/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb
+++ b/spec/workers/clusters/cleanup/project_namespace_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Cleanup::ProjectNamespaceWorker do
+RSpec.describe Clusters::Cleanup::ProjectNamespaceWorker, feature_category: :kubernetes_management do
describe '#perform' do
context 'when cluster.cleanup_status is cleanup_removing_project_namespaces' do
let!(:cluster) { create(:cluster, :with_environments, :cleanup_removing_project_namespaces) }
diff --git a/spec/workers/clusters/cleanup/service_account_worker_spec.rb b/spec/workers/clusters/cleanup/service_account_worker_spec.rb
index dabc32a0ccd..0d4df795278 100644
--- a/spec/workers/clusters/cleanup/service_account_worker_spec.rb
+++ b/spec/workers/clusters/cleanup/service_account_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Cleanup::ServiceAccountWorker do
+RSpec.describe Clusters::Cleanup::ServiceAccountWorker, feature_category: :kubernetes_management do
describe '#perform' do
let!(:cluster) { create(:cluster, :cleanup_removing_service_account) }
diff --git a/spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb b/spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb
index 6f70870bd09..1f5892a36da 100644
--- a/spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb
+++ b/spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Clusters::Integrations::CheckPrometheusHealthWorker, '#perform' do
+RSpec.describe Clusters::Integrations::CheckPrometheusHealthWorker, '#perform', feature_category: :incident_management do
subject { described_class.new.perform }
it 'triggers health service' do
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 0abb029f146..e4df91adef2 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ApplicationWorker do
+RSpec.describe ApplicationWorker, feature_category: :shared do
# We depend on the lazy-load characteristic of rspec. If the worker is loaded
# before setting up, it's likely to go wrong. Consider this catcha:
# before do
diff --git a/spec/workers/concerns/cluster_agent_queue_spec.rb b/spec/workers/concerns/cluster_agent_queue_spec.rb
index b5189cbd8c8..c30616d04e1 100644
--- a/spec/workers/concerns/cluster_agent_queue_spec.rb
+++ b/spec/workers/concerns/cluster_agent_queue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ClusterAgentQueue do
+RSpec.describe ClusterAgentQueue, feature_category: :kubernetes_management do
let(:worker) do
Class.new do
def self.name
@@ -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..7e00093b686 100644
--- a/spec/workers/concerns/cronjob_queue_spec.rb
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CronjobQueue do
+RSpec.describe CronjobQueue, feature_category: :shared do
let(:worker) do
Class.new do
def self.name
@@ -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/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index 02190201986..f72caf3a8c2 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
+RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures, feature_category: :importers do
let(:worker) do
Class.new do
def self.name
@@ -196,6 +196,19 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
end
context 'when the record is invalid' do
+ let(:exception) { ActiveRecord::RecordInvalid.new }
+
+ before do
+ expect(importer_class)
+ .to receive(:new)
+ .with(instance_of(MockRepresantation), project, client)
+ .and_return(importer_instance)
+
+ expect(importer_instance)
+ .to receive(:execute)
+ .and_raise(exception)
+ end
+
it 'logs an error' do
expect(Gitlab::GithubImport::Logger)
.to receive(:info)
@@ -208,16 +221,6 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
}
)
- expect(importer_class)
- .to receive(:new)
- .with(instance_of(MockRepresantation), project, client)
- .and_return(importer_instance)
-
- exception = ActiveRecord::RecordInvalid.new
- expect(importer_instance)
- .to receive(:execute)
- .and_raise(exception)
-
expect(Gitlab::Import::ImportFailureService)
.to receive(:track)
.with(
@@ -230,6 +233,15 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
worker.import(project, client, { 'number' => 10, 'github_id' => 1 })
end
+
+ it 'updates external_identifiers of the correct failure' do
+ worker.import(project, client, { 'number' => 10, 'github_id' => 1 })
+
+ import_failures = project.import_failures
+
+ expect(import_failures.count).to eq(1)
+ expect(import_failures.first.external_identifiers).to eq(github_identifiers.with_indifferent_access)
+ end
end
end
@@ -240,4 +252,56 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
expect(worker).to be_increment_object_counter(issue)
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:correlation_id) { 'abc' }
+ let(:job) do
+ {
+ 'args' => [project.id, { number: 123, state: 'open' }, '123abc'],
+ 'jid' => '123',
+ 'correlation_id' => correlation_id
+ }
+ end
+
+ subject(:sidekiq_retries_exhausted) { worker.class.sidekiq_retries_exhausted_block.call(job, StandardError.new) }
+
+ context 'when all arguments are given' do
+ it 'notifies the JobWaiter' do
+ expect(Gitlab::JobWaiter)
+ .to receive(:notify)
+ .with(
+ job['args'].last,
+ job['jid']
+ )
+
+ sidekiq_retries_exhausted
+ end
+ end
+
+ context 'when not all arguments are given' do
+ let(:job) do
+ {
+ 'args' => [project.id, { number: 123, state: 'open' }],
+ 'jid' => '123',
+ 'correlation_id' => correlation_id
+ }
+ end
+
+ it 'does not notify the JobWaiter' do
+ expect(Gitlab::JobWaiter).not_to receive(:notify)
+
+ sidekiq_retries_exhausted
+ end
+ end
+
+ it 'updates external_identifiers of the correct failure' do
+ failure_1, failure_2 = create_list(:import_failure, 2, project: project)
+ failure_2.update_column(:correlation_id_value, correlation_id)
+
+ sidekiq_retries_exhausted
+
+ expect(failure_1.reload.external_identifiers).to be_empty
+ expect(failure_2.reload.external_identifiers).to eq(github_identifiers.with_indifferent_access)
+ end
+ end
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/gitlab/github_import/rescheduling_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
index 8727756ce50..f7ba368f312 100644
--- a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ReschedulingMethods do
+RSpec.describe Gitlab::GithubImport::ReschedulingMethods, feature_category: :importers do
let(:worker) do
Class.new { include(Gitlab::GithubImport::ReschedulingMethods) }.new
end
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
index 0ac1733781a..ce9a9db5dd9 100644
--- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::StageMethods do
+RSpec.describe Gitlab::GithubImport::StageMethods, feature_category: :importers do
let_it_be(:project) { create(:project, :import_started, import_url: 'https://t0ken@github.com/repo/repo.git') }
let_it_be(:project2) { create(:project, :import_canceled) }
diff --git a/spec/workers/concerns/gitlab/notify_upon_death_spec.rb b/spec/workers/concerns/gitlab/notify_upon_death_spec.rb
index dd0a1cadc9c..36faf3ee296 100644
--- a/spec/workers/concerns/gitlab/notify_upon_death_spec.rb
+++ b/spec/workers/concerns/gitlab/notify_upon_death_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::NotifyUponDeath do
+RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do
let(:worker_class) do
Class.new do
include Sidekiq::Worker
diff --git a/spec/workers/concerns/limited_capacity/job_tracker_spec.rb b/spec/workers/concerns/limited_capacity/job_tracker_spec.rb
index 0e3fa350fcd..20635d1a045 100644
--- a/spec/workers/concerns/limited_capacity/job_tracker_spec.rb
+++ b/spec/workers/concerns/limited_capacity/job_tracker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LimitedCapacity::JobTracker, :clean_gitlab_redis_shared_state do
+RSpec.describe LimitedCapacity::JobTracker, :clean_gitlab_redis_shared_state, feature_category: :shared do
let(:job_tracker) do
described_class.new('namespace')
end
diff --git a/spec/workers/concerns/limited_capacity/worker_spec.rb b/spec/workers/concerns/limited_capacity/worker_spec.rb
index 790b5c3544d..65906eef0fa 100644
--- a/spec/workers/concerns/limited_capacity/worker_spec.rb
+++ b/spec/workers/concerns/limited_capacity/worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LimitedCapacity::Worker, :clean_gitlab_redis_queues, :aggregate_failures do
+RSpec.describe LimitedCapacity::Worker, :clean_gitlab_redis_queues, :aggregate_failures, feature_category: :shared do
let(:worker_class) do
Class.new do
def self.name
diff --git a/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb b/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb
index 95962d4810e..daecda8c92e 100644
--- a/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb
+++ b/spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Packages::CleanupArtifactWorker do
+RSpec.describe ::Packages::CleanupArtifactWorker, feature_category: :build_artifacts do
let_it_be(:worker_class) do
Class.new do
def self.name
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/project_import_options_spec.rb b/spec/workers/concerns/project_import_options_spec.rb
index 85a26ddb0cb..51d7f9fdea4 100644
--- a/spec/workers/concerns/project_import_options_spec.rb
+++ b/spec/workers/concerns/project_import_options_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectImportOptions do
+RSpec.describe ProjectImportOptions, feature_category: :importers do
let(:project) { create(:project, :import_started) }
let(:job) { { 'args' => [project.id, nil, nil], 'jid' => '123' } }
let(:worker_class) do
diff --git a/spec/workers/concerns/reenqueuer_spec.rb b/spec/workers/concerns/reenqueuer_spec.rb
index e7287b55af2..42873578014 100644
--- a/spec/workers/concerns/reenqueuer_spec.rb
+++ b/spec/workers/concerns/reenqueuer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Reenqueuer do
+RSpec.describe Reenqueuer, feature_category: :shared do
include ExclusiveLeaseHelpers
let_it_be(:worker_class) do
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
index ae377c09b37..12082e2dff5 100644
--- a/spec/workers/concerns/repository_check_queue_spec.rb
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryCheckQueue do
+RSpec.describe RepositoryCheckQueue, feature_category: :source_code_management do
let(:worker) do
Class.new do
def self.name
@@ -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 737424ffd8c..9cb34bbbab9 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WaitableWorker do
+RSpec.describe WaitableWorker, feature_category: :shared do
let(:worker) do
Class.new do
def self.name
diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb
index 5e8f68923fd..ac9d3fa824e 100644
--- a/spec/workers/concerns/worker_attributes_spec.rb
+++ b/spec/workers/concerns/worker_attributes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkerAttributes do
+RSpec.describe WorkerAttributes, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
let(:worker) do
diff --git a/spec/workers/concerns/worker_context_spec.rb b/spec/workers/concerns/worker_context_spec.rb
index 80b427b2b42..0bbe14842bb 100644
--- a/spec/workers/concerns/worker_context_spec.rb
+++ b/spec/workers/concerns/worker_context_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkerContext do
+RSpec.describe WorkerContext, feature_category: :shared do
let(:worker) do
Class.new do
def self.name
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index 8eda943f36e..8dae859b168 100644
--- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
+RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
let_it_be(:repository, refind: true) { create(:container_repository, :cleanup_scheduled, expiration_policy_started_at: 1.month.ago) }
@@ -348,16 +348,18 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
subject { worker.send(:container_repository) }
- if params[:expected_selected_repository] == :none
- it 'does not select any repository' do
+ it 'selects the correct repository', :freeze_time do
+ case expected_selected_repository
+ when :none
expect(subject).to eq(nil)
+ next
+ when :repository
+ expect(subject).to eq(repository)
+ when :other_repository
+ expect(subject).to eq(other_repository)
end
- else
- it 'does select a repository' do
- selected_repository = expected_selected_repository == :repository ? repository : other_repository
-
- expect(subject).to eq(selected_repository)
- end
+ expect(subject).to be_cleanup_ongoing
+ expect(subject.expiration_policy_started_at).to eq(Time.zone.now)
end
def update_container_repository(container_repository, cleanup_status, policy_status)
@@ -511,6 +513,16 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
subject
end
end
+
+ context 'with a stuck container repository' do
+ before do
+ repository.cleanup_ongoing!
+ repository.update_column(:expiration_policy_started_at, nil)
+ policy.update_column(:next_run_at, 5.minutes.ago)
+ end
+
+ it { is_expected.to eq(0) }
+ end
end
describe '#max_running_jobs' do
diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb
index ef6266aeba3..fcb43b86084 100644
--- a/spec/workers/container_expiration_policy_worker_spec.rb
+++ b/spec/workers/container_expiration_policy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerExpirationPolicyWorker do
+RSpec.describe ContainerExpirationPolicyWorker, feature_category: :container_registry do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
@@ -33,15 +33,17 @@ RSpec.describe ContainerExpirationPolicyWorker do
end
context 'process stale ongoing cleanups' do
- let_it_be(:stuck_cleanup) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
+ let_it_be(:stuck_cleanup1) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
+ let_it_be(:stuck_cleanup2) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: nil) }
let_it_be(:container_repository1) { create(:container_repository, :cleanup_scheduled) }
let_it_be(:container_repository2) { create(:container_repository, :cleanup_unfinished) }
it 'set them as unfinished' do
expect { subject }
- .to change { ContainerRepository.cleanup_ongoing.count }.from(1).to(0)
- .and change { ContainerRepository.cleanup_unfinished.count }.from(1).to(2)
- expect(stuck_cleanup.reload).to be_cleanup_unfinished
+ .to change { ContainerRepository.cleanup_ongoing.count }.from(2).to(0)
+ .and change { ContainerRepository.cleanup_unfinished.count }.from(1).to(3)
+ expect(stuck_cleanup1.reload).to be_cleanup_unfinished
+ expect(stuck_cleanup2.reload).to be_cleanup_unfinished
end
end
diff --git a/spec/workers/container_registry/cleanup_worker_spec.rb b/spec/workers/container_registry/cleanup_worker_spec.rb
index a510b660412..72e1243ccb5 100644
--- a/spec/workers/container_registry/cleanup_worker_spec.rb
+++ b/spec/workers/container_registry/cleanup_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::CleanupWorker, :aggregate_failures do
+RSpec.describe ContainerRegistry::CleanupWorker, :aggregate_failures, feature_category: :container_registry do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/container_registry/delete_container_repository_worker_spec.rb b/spec/workers/container_registry/delete_container_repository_worker_spec.rb
index 381e0cc164c..8bab0133d55 100644
--- a/spec/workers/container_registry/delete_container_repository_worker_spec.rb
+++ b/spec/workers/container_registry/delete_container_repository_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::DeleteContainerRepositoryWorker, :aggregate_failures do
+RSpec.describe ContainerRegistry::DeleteContainerRepositoryWorker, :aggregate_failures, feature_category: :container_registry do
let_it_be_with_reload(:container_repository) { create(:container_repository) }
let_it_be(:second_container_repository) { create(:container_repository) }
diff --git a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
index c2381c0ced7..4a603e538ef 100644
--- a/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
+++ b/spec/workers/container_registry/migration/enqueuer_worker_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures, :clean_gitlab_redis_shared_state do
+RSpec.describe ContainerRegistry::Migration::EnqueuerWorker, :aggregate_failures, :clean_gitlab_redis_shared_state,
+ feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
include ExclusiveLeaseHelpers
diff --git a/spec/workers/container_registry/migration/guard_worker_spec.rb b/spec/workers/container_registry/migration/guard_worker_spec.rb
index 4ad2d5c300c..40ade93ab0d 100644
--- a/spec/workers/container_registry/migration/guard_worker_spec.rb
+++ b/spec/workers/container_registry/migration/guard_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures do
+RSpec.describe ContainerRegistry::Migration::GuardWorker, :aggregate_failures, feature_category: :container_registry do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/container_registry/migration/observer_worker_spec.rb b/spec/workers/container_registry/migration/observer_worker_spec.rb
index fec6640d7ec..7bf3d90d9d3 100644
--- a/spec/workers/container_registry/migration/observer_worker_spec.rb
+++ b/spec/workers/container_registry/migration/observer_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRegistry::Migration::ObserverWorker, :aggregate_failures do
+RSpec.describe ContainerRegistry::Migration::ObserverWorker, :aggregate_failures, feature_category: :container_registry do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/counters/cleanup_refresh_worker_spec.rb b/spec/workers/counters/cleanup_refresh_worker_spec.rb
index a56c98f72a0..a18f78f1706 100644
--- a/spec/workers/counters/cleanup_refresh_worker_spec.rb
+++ b/spec/workers/counters/cleanup_refresh_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Counters::CleanupRefreshWorker do
+RSpec.describe Counters::CleanupRefreshWorker, feature_category: :shared do
let(:model) { create(:project_statistics) }
describe '#perform', :redis do
diff --git a/spec/workers/create_commit_signature_worker_spec.rb b/spec/workers/create_commit_signature_worker_spec.rb
index 9d3c63efc8a..722632b6796 100644
--- a/spec/workers/create_commit_signature_worker_spec.rb
+++ b/spec/workers/create_commit_signature_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CreateCommitSignatureWorker do
+RSpec.describe CreateCommitSignatureWorker, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:commits) { project.repository.commits('HEAD', limit: 3).commits }
let(:commit_shas) { commits.map(&:id) }
diff --git a/spec/workers/create_note_diff_file_worker_spec.rb b/spec/workers/create_note_diff_file_worker_spec.rb
index 6d1d6d93e44..6e4a5e3ab6a 100644
--- a/spec/workers/create_note_diff_file_worker_spec.rb
+++ b/spec/workers/create_note_diff_file_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CreateNoteDiffFileWorker do
+RSpec.describe CreateNoteDiffFileWorker, feature_category: :code_review_workflow do
describe '#perform' do
let(:diff_note) { create(:diff_note_on_merge_request) }
diff --git a/spec/workers/create_pipeline_worker_spec.rb b/spec/workers/create_pipeline_worker_spec.rb
index da85d700429..23a2dc075a7 100644
--- a/spec/workers/create_pipeline_worker_spec.rb
+++ b/spec/workers/create_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CreatePipelineWorker do
+RSpec.describe CreatePipelineWorker, feature_category: :continuous_integration do
describe '#perform' do
let(:worker) { described_class.new }
diff --git a/spec/workers/database/batched_background_migration/ci_database_worker_spec.rb b/spec/workers/database/batched_background_migration/ci_database_worker_spec.rb
index dfe7a266be2..91ba6e5a20a 100644
--- a/spec/workers/database/batched_background_migration/ci_database_worker_spec.rb
+++ b/spec/workers/database/batched_background_migration/ci_database_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe Database::BatchedBackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe Database::BatchedBackgroundMigration::CiDatabaseWorker, :clean_gitlab_redis_shared_state, feature_category: :database do
it_behaves_like 'it runs batched background migration jobs', :ci, :ci_builds
end
diff --git a/spec/workers/database/batched_background_migration_worker_spec.rb b/spec/workers/database/batched_background_migration_worker_spec.rb
index e57bd7581c2..a6825c5ca76 100644
--- a/spec/workers/database/batched_background_migration_worker_spec.rb
+++ b/spec/workers/database/batched_background_migration_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe Database::BatchedBackgroundMigrationWorker do
+RSpec.describe Database::BatchedBackgroundMigrationWorker, feature_category: :database do
it_behaves_like 'it runs batched background migration jobs', :main, :events
end
diff --git a/spec/workers/database/ci_namespace_mirrors_consistency_check_worker_spec.rb b/spec/workers/database/ci_namespace_mirrors_consistency_check_worker_spec.rb
index 1c083d1d8a3..6b6723a468f 100644
--- a/spec/workers/database/ci_namespace_mirrors_consistency_check_worker_spec.rb
+++ b/spec/workers/database/ci_namespace_mirrors_consistency_check_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Database::CiNamespaceMirrorsConsistencyCheckWorker do
+RSpec.describe Database::CiNamespaceMirrorsConsistencyCheckWorker, feature_category: :pods do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb b/spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb
index 8c839410ccd..613d40b57d8 100644
--- a/spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb
+++ b/spec/workers/database/ci_project_mirrors_consistency_check_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Database::CiProjectMirrorsConsistencyCheckWorker do
+RSpec.describe Database::CiProjectMirrorsConsistencyCheckWorker, feature_category: :pods do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/database/drop_detached_partitions_worker_spec.rb b/spec/workers/database/drop_detached_partitions_worker_spec.rb
index a10fcaaa5d9..2ab77e71070 100644
--- a/spec/workers/database/drop_detached_partitions_worker_spec.rb
+++ b/spec/workers/database/drop_detached_partitions_worker_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Database::DropDetachedPartitionsWorker do
+RSpec.describe Database::DropDetachedPartitionsWorker, feature_category: :database do
describe '#perform' do
subject { described_class.new.perform }
diff --git a/spec/workers/database/partition_management_worker_spec.rb b/spec/workers/database/partition_management_worker_spec.rb
index e5362e95f48..203181ef28d 100644
--- a/spec/workers/database/partition_management_worker_spec.rb
+++ b/spec/workers/database/partition_management_worker_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Database::PartitionManagementWorker do
+RSpec.describe Database::PartitionManagementWorker, feature_category: :database do
describe '#perform' do
subject { described_class.new.perform }
diff --git a/spec/workers/delete_container_repository_worker_spec.rb b/spec/workers/delete_container_repository_worker_spec.rb
index 6ad131b4c14..6260bea6949 100644
--- a/spec/workers/delete_container_repository_worker_spec.rb
+++ b/spec/workers/delete_container_repository_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeleteContainerRepositoryWorker do
+RSpec.describe DeleteContainerRepositoryWorker, feature_category: :container_registry do
let_it_be(:repository) { create(:container_repository) }
let(:project) { repository.project }
diff --git a/spec/workers/delete_diff_files_worker_spec.rb b/spec/workers/delete_diff_files_worker_spec.rb
index c124847ca45..1f1b00e324e 100644
--- a/spec/workers/delete_diff_files_worker_spec.rb
+++ b/spec/workers/delete_diff_files_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeleteDiffFilesWorker do
+RSpec.describe DeleteDiffFilesWorker, feature_category: :code_review_workflow do
describe '#perform' do
let(:merge_request) { create(:merge_request) }
let(:merge_request_diff) { merge_request.merge_request_diff }
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
index 056fcb1200d..f3b6dccad2e 100644
--- a/spec/workers/delete_merged_branches_worker_spec.rb
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeleteMergedBranchesWorker do
+RSpec.describe DeleteMergedBranchesWorker, feature_category: :source_code_management do
subject(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 4046b670640..05c2b0e7930 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DeleteUserWorker do
+RSpec.describe DeleteUserWorker, feature_category: :user_management do
let!(:user) { create(:user) }
let!(:current_user) { create(:user) }
diff --git a/spec/workers/dependency_proxy/cleanup_blob_worker_spec.rb b/spec/workers/dependency_proxy/cleanup_blob_worker_spec.rb
index b67a56cca7b..9ef5fd9ec93 100644
--- a/spec/workers/dependency_proxy/cleanup_blob_worker_spec.rb
+++ b/spec/workers/dependency_proxy/cleanup_blob_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DependencyProxy::CleanupBlobWorker do
+RSpec.describe DependencyProxy::CleanupBlobWorker, feature_category: :dependency_proxy do
let_it_be(:factory_type) { :dependency_proxy_blob }
it_behaves_like 'dependency_proxy_cleanup_worker'
diff --git a/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb b/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb
index 1100f9a7fae..3040189d1c8 100644
--- a/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb
+++ b/spec/workers/dependency_proxy/cleanup_dependency_proxy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DependencyProxy::CleanupDependencyProxyWorker do
+RSpec.describe DependencyProxy::CleanupDependencyProxyWorker, feature_category: :dependency_proxy do
describe '#perform' do
subject { described_class.new.perform }
diff --git a/spec/workers/dependency_proxy/cleanup_manifest_worker_spec.rb b/spec/workers/dependency_proxy/cleanup_manifest_worker_spec.rb
index d53b3e6a1fd..730acc49110 100644
--- a/spec/workers/dependency_proxy/cleanup_manifest_worker_spec.rb
+++ b/spec/workers/dependency_proxy/cleanup_manifest_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DependencyProxy::CleanupManifestWorker do
+RSpec.describe DependencyProxy::CleanupManifestWorker, feature_category: :dependency_proxy do
let_it_be(:factory_type) { :dependency_proxy_manifest }
it_behaves_like 'dependency_proxy_cleanup_worker'
diff --git a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
index 6a2fdfbe8f5..44a21439ff8 100644
--- a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
+++ b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DependencyProxy::ImageTtlGroupPolicyWorker do
+RSpec.describe DependencyProxy::ImageTtlGroupPolicyWorker, feature_category: :dependency_proxy do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/deployments/archive_in_project_worker_spec.rb b/spec/workers/deployments/archive_in_project_worker_spec.rb
index 6435fe8bea1..2d244344913 100644
--- a/spec/workers/deployments/archive_in_project_worker_spec.rb
+++ b/spec/workers/deployments/archive_in_project_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::ArchiveInProjectWorker do
+RSpec.describe Deployments::ArchiveInProjectWorker, feature_category: :continuous_delivery do
subject { described_class.new.perform(deployment&.project_id) }
describe '#perform' do
diff --git a/spec/workers/deployments/drop_older_deployments_worker_spec.rb b/spec/workers/deployments/drop_older_deployments_worker_spec.rb
index 0cf524ca16f..8637a7788cc 100644
--- a/spec/workers/deployments/drop_older_deployments_worker_spec.rb
+++ b/spec/workers/deployments/drop_older_deployments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::DropOlderDeploymentsWorker do
+RSpec.describe Deployments::DropOlderDeploymentsWorker, feature_category: :continuous_delivery do
subject { described_class.new.perform(deployment&.id) }
describe '#perform' do
diff --git a/spec/workers/deployments/hooks_worker_spec.rb b/spec/workers/deployments/hooks_worker_spec.rb
index 7c5f288fa57..51614f8b0cb 100644
--- a/spec/workers/deployments/hooks_worker_spec.rb
+++ b/spec/workers/deployments/hooks_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::HooksWorker do
+RSpec.describe Deployments::HooksWorker, feature_category: :continuous_delivery do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/deployments/link_merge_request_worker_spec.rb b/spec/workers/deployments/link_merge_request_worker_spec.rb
index a55dd897bc7..0e484c50f21 100644
--- a/spec/workers/deployments/link_merge_request_worker_spec.rb
+++ b/spec/workers/deployments/link_merge_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::LinkMergeRequestWorker do
+RSpec.describe Deployments::LinkMergeRequestWorker, feature_category: :continuous_delivery do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/deployments/update_environment_worker_spec.rb b/spec/workers/deployments/update_environment_worker_spec.rb
index d67cbd62616..befe8576e88 100644
--- a/spec/workers/deployments/update_environment_worker_spec.rb
+++ b/spec/workers/deployments/update_environment_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployments::UpdateEnvironmentWorker do
+RSpec.describe Deployments::UpdateEnvironmentWorker, feature_category: :continuous_delivery do
subject(:worker) { described_class.new }
context 'when successful deployment' do
diff --git a/spec/workers/design_management/copy_design_collection_worker_spec.rb b/spec/workers/design_management/copy_design_collection_worker_spec.rb
index 45bfc47ca7e..daa69a8bd6d 100644
--- a/spec/workers/design_management/copy_design_collection_worker_spec.rb
+++ b/spec/workers/design_management/copy_design_collection_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DesignManagement::CopyDesignCollectionWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe DesignManagement::CopyDesignCollectionWorker, :clean_gitlab_redis_shared_state, feature_category: :design_management do
describe '#perform' do
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue) }
diff --git a/spec/workers/design_management/new_version_worker_spec.rb b/spec/workers/design_management/new_version_worker_spec.rb
index 3320d7a062d..afc908d925a 100644
--- a/spec/workers/design_management/new_version_worker_spec.rb
+++ b/spec/workers/design_management/new_version_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DesignManagement::NewVersionWorker do
+RSpec.describe DesignManagement::NewVersionWorker, feature_category: :design_management do
describe '#perform' do
let(:worker) { described_class.new }
diff --git a/spec/workers/destroy_pages_deployments_worker_spec.rb b/spec/workers/destroy_pages_deployments_worker_spec.rb
index 2c20c9004ef..4bfde6d220a 100644
--- a/spec/workers/destroy_pages_deployments_worker_spec.rb
+++ b/spec/workers/destroy_pages_deployments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DestroyPagesDeploymentsWorker do
+RSpec.describe DestroyPagesDeploymentsWorker, feature_category: :pages do
subject(:worker) { described_class.new }
let(:project) { create(:project) }
diff --git a/spec/workers/detect_repository_languages_worker_spec.rb b/spec/workers/detect_repository_languages_worker_spec.rb
index 217e16bd155..27d60720d24 100644
--- a/spec/workers/detect_repository_languages_worker_spec.rb
+++ b/spec/workers/detect_repository_languages_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DetectRepositoryLanguagesWorker do
+RSpec.describe DetectRepositoryLanguagesWorker, feature_category: :source_code_management do
let_it_be(:project) { create(:project) }
subject { described_class.new }
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 3a875727cce..c732f8a3d00 100644
--- a/spec/workers/disallow_two_factor_for_group_worker_spec.rb
+++ b/spec/workers/disallow_two_factor_for_group_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DisallowTwoFactorForGroupWorker do
+RSpec.describe DisallowTwoFactorForGroupWorker, feature_category: :subgroups do
let_it_be(:group) { create(:group, require_two_factor_authentication: true) }
let_it_be(:user) { create(:user, require_two_factor_authentication_from_group: true) }
diff --git a/spec/workers/disallow_two_factor_for_subgroups_worker_spec.rb b/spec/workers/disallow_two_factor_for_subgroups_worker_spec.rb
index c3be8263171..7584355deab 100644
--- a/spec/workers/disallow_two_factor_for_subgroups_worker_spec.rb
+++ b/spec/workers/disallow_two_factor_for_subgroups_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DisallowTwoFactorForSubgroupsWorker do
+RSpec.describe DisallowTwoFactorForSubgroupsWorker, feature_category: :subgroups do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup_with_2fa) { create(:group, parent: group, require_two_factor_authentication: true) }
let_it_be(:subgroup_without_2fa) { create(:group, parent: group, require_two_factor_authentication: false) }
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index dba535654a1..4c464c797e4 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe EmailReceiverWorker, :mailer do
+RSpec.describe EmailReceiverWorker, :mailer, feature_category: :team_planning do
let(:raw_message) { fixture_file('emails/valid_reply.eml') }
context "when reply by email is enabled" do
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 7d11957e2df..9e8fad19c20 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,12 +2,12 @@
require 'spec_helper'
-RSpec.describe EmailsOnPushWorker, :mailer do
- include RepoHelpers
+RSpec.describe EmailsOnPushWorker, :mailer, feature_category: :source_code_management do
include EmailSpec::Matchers
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
@@ -91,7 +91,7 @@ RSpec.describe EmailsOnPushWorker, :mailer do
context "when there is an SMTP error" do
before do
- allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError.new(nil))
allow(subject).to receive_message_chain(:logger, :info)
perform
end
diff --git a/spec/workers/environments/auto_delete_cron_worker_spec.rb b/spec/workers/environments/auto_delete_cron_worker_spec.rb
index b18f3da5d10..d7e707b225e 100644
--- a/spec/workers/environments/auto_delete_cron_worker_spec.rb
+++ b/spec/workers/environments/auto_delete_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::AutoDeleteCronWorker do
+RSpec.describe Environments::AutoDeleteCronWorker, feature_category: :continuous_delivery do
include CreateEnvironmentsHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/environments/auto_stop_cron_worker_spec.rb b/spec/workers/environments/auto_stop_cron_worker_spec.rb
index 1e86597d288..ad44cf97e07 100644
--- a/spec/workers/environments/auto_stop_cron_worker_spec.rb
+++ b/spec/workers/environments/auto_stop_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::AutoStopCronWorker do
+RSpec.describe Environments::AutoStopCronWorker, feature_category: :continuous_delivery do
subject { worker.perform }
let(:worker) { described_class.new }
diff --git a/spec/workers/environments/auto_stop_worker_spec.rb b/spec/workers/environments/auto_stop_worker_spec.rb
index cb162b5a01c..cfdca4a385c 100644
--- a/spec/workers/environments/auto_stop_worker_spec.rb
+++ b/spec/workers/environments/auto_stop_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::AutoStopWorker do
+RSpec.describe Environments::AutoStopWorker, feature_category: :continuous_delivery do
include CreateEnvironmentsHelpers
subject { worker.perform(environment_id) }
diff --git a/spec/workers/environments/canary_ingress/update_worker_spec.rb b/spec/workers/environments/canary_ingress/update_worker_spec.rb
index e7782c2fba1..abfca54c850 100644
--- a/spec/workers/environments/canary_ingress/update_worker_spec.rb
+++ b/spec/workers/environments/canary_ingress/update_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Environments::CanaryIngress::UpdateWorker do
+RSpec.describe Environments::CanaryIngress::UpdateWorker, feature_category: :continuous_delivery do
let_it_be(:environment) { create(:environment) }
let(:worker) { described_class.new }
diff --git a/spec/workers/error_tracking_issue_link_worker_spec.rb b/spec/workers/error_tracking_issue_link_worker_spec.rb
index 90e747c8788..fcb39597f9e 100644
--- a/spec/workers/error_tracking_issue_link_worker_spec.rb
+++ b/spec/workers/error_tracking_issue_link_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ErrorTrackingIssueLinkWorker do
+RSpec.describe ErrorTrackingIssueLinkWorker, feature_category: :error_tracking do
let_it_be(:error_tracking) { create(:project_error_tracking_setting) }
let_it_be(:project) { error_tracking.project }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index c0d46a206ce..f080e2ef1c3 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Every Sidekiq worker' do
+RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
let(:workers_without_defaults) do
Gitlab::SidekiqConfig.workers - Gitlab::SidekiqConfig::DEFAULT_WORKERS.values
end
@@ -272,6 +272,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::GithubImport::ImportLfsObjectWorker' => 5,
'Gitlab::GithubImport::ImportNoteWorker' => 5,
'Gitlab::GithubImport::ImportProtectedBranchWorker' => 5,
+ 'Gitlab::GithubImport::ImportCollaboratorWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestMergedByWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestReviewWorker' => 5,
'Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker' => 5,
@@ -285,6 +286,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::GithubImport::Stage::ImportAttachmentsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker' => 5,
'Gitlab::GithubImport::Stage::ImportNotesWorker' => 5,
+ 'Gitlab::GithubImport::Stage::ImportCollaboratorsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker' => 5,
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index 3f8da3fb71c..2ef832eaaed 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ExpireBuildArtifactsWorker do
+RSpec.describe ExpireBuildArtifactsWorker, feature_category: :build_artifacts do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/export_csv_worker_spec.rb b/spec/workers/export_csv_worker_spec.rb
index 88ccfac0a02..2de6c58958f 100644
--- a/spec/workers/export_csv_worker_spec.rb
+++ b/spec/workers/export_csv_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ExportCsvWorker do
+RSpec.describe ExportCsvWorker, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, creator: user) }
diff --git a/spec/workers/external_service_reactive_caching_worker_spec.rb b/spec/workers/external_service_reactive_caching_worker_spec.rb
index 907894d9b9a..58d9bec343a 100644
--- a/spec/workers/external_service_reactive_caching_worker_spec.rb
+++ b/spec/workers/external_service_reactive_caching_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe ExternalServiceReactiveCachingWorker do
+RSpec.describe ExternalServiceReactiveCachingWorker, feature_category: :shared do
it_behaves_like 'reactive cacheable worker'
end
diff --git a/spec/workers/file_hook_worker_spec.rb b/spec/workers/file_hook_worker_spec.rb
index c171dc37e5f..00cd0e9c98e 100644
--- a/spec/workers/file_hook_worker_spec.rb
+++ b/spec/workers/file_hook_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FileHookWorker do
+RSpec.describe FileHookWorker, feature_category: :integrations do
include RepoHelpers
let(:filename) { 'my_file_hook.rb' }
diff --git a/spec/workers/flush_counter_increments_worker_spec.rb b/spec/workers/flush_counter_increments_worker_spec.rb
index 83670acf4b6..1bdda3100c9 100644
--- a/spec/workers/flush_counter_increments_worker_spec.rb
+++ b/spec/workers/flush_counter_increments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FlushCounterIncrementsWorker, :counter_attribute do
+RSpec.describe FlushCounterIncrementsWorker, :counter_attribute, feature_category: :shared do
let(:project_statistics) { create(:project_statistics) }
let(:model) { CounterAttributeModel.find(project_statistics.id) }
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
index 4e8261f61c4..121f30ea9d5 100644
--- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state, feature_category: :importers do
let(:project) { create(:project) }
let(:import_state) { create(:import_state, project: project, jid: '123') }
let(:worker) { described_class.new }
diff --git a/spec/workers/gitlab/github_import/attachments/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/attachments/import_issue_worker_spec.rb
index 6d617755861..fc03e14c20e 100644
--- a/spec/workers/gitlab/github_import/attachments/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/attachments/import_issue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Attachments::ImportIssueWorker do
+RSpec.describe Gitlab::GithubImport::Attachments::ImportIssueWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/attachments/import_merge_request_worker_spec.rb b/spec/workers/gitlab/github_import/attachments/import_merge_request_worker_spec.rb
index 66dfc027e6e..bd90cee567e 100644
--- a/spec/workers/gitlab/github_import/attachments/import_merge_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/attachments/import_merge_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Attachments::ImportMergeRequestWorker do
+RSpec.describe Gitlab::GithubImport::Attachments::ImportMergeRequestWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/attachments/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/attachments/import_note_worker_spec.rb
index 7b60cdecca6..7d8fb9bc788 100644
--- a/spec/workers/gitlab/github_import/attachments/import_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/attachments/import_note_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Attachments::ImportNoteWorker do
+RSpec.describe Gitlab::GithubImport::Attachments::ImportNoteWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/attachments/import_release_worker_spec.rb b/spec/workers/gitlab/github_import/attachments/import_release_worker_spec.rb
index e49b2fb6504..50eebc6ce8c 100644
--- a/spec/workers/gitlab/github_import/attachments/import_release_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/attachments/import_release_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Attachments::ImportReleaseWorker do
+RSpec.describe Gitlab::GithubImport::Attachments::ImportReleaseWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/import_collaborator_worker_spec.rb b/spec/workers/gitlab/github_import/import_collaborator_worker_spec.rb
new file mode 100644
index 00000000000..b9463fb9a2d
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_collaborator_worker_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::ImportCollaboratorWorker, feature_category: :importers do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ let(:project) do
+ instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state)
+ end
+
+ let(:import_state) { build_stubbed(:import_state, :started) }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:importer) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
+
+ it 'imports a collaborator' do
+ expect(Gitlab::GithubImport::Importer::CollaboratorImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Collaborator),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment)
+ .and_call_original
+
+ worker.import(
+ project, client, { 'id' => 100500, 'login' => 'alice', 'role_name' => 'maintainer' }
+ )
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
index c92741e8f10..35d31848fe1 100644
--- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportDiffNoteWorker do
+RSpec.describe Gitlab::GithubImport::ImportDiffNoteWorker, feature_category: :importers do
let(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb
index 03a6503fb84..aa8243154ef 100644
--- a/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_event_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportIssueEventWorker do
+RSpec.describe Gitlab::GithubImport::ImportIssueEventWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
index ef1d2e3f3e7..76f0b512af4 100644
--- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportIssueWorker do
+RSpec.describe Gitlab::GithubImport::ImportIssueWorker, feature_category: :importers do
let(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
index 16ca5658f77..b11ea560516 100644
--- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportNoteWorker do
+RSpec.describe Gitlab::GithubImport::ImportNoteWorker, feature_category: :importers do
let(:worker) { described_class.new }
describe '#import' do
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
index c2b8ee661a3..d6e8f760033 100644
--- a/spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_protected_branch_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportProtectedBranchWorker do
+RSpec.describe Gitlab::GithubImport::ImportProtectedBranchWorker, feature_category: :importers do
let(:worker) { described_class.new }
let(:import_state) { build_stubbed(:import_state, :started) }
diff --git a/spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb
index 728b4c6b440..6f771a1b79d 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_merged_by_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportPullRequestMergedByWorker do
+RSpec.describe Gitlab::GithubImport::ImportPullRequestMergedByWorker, feature_category: :importers do
it { is_expected.to include_module(Gitlab::GithubImport::ObjectImporter) }
describe '#representation_class' do
diff --git a/spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb
index 0607add52cd..ede74a75ce5 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_review_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportPullRequestReviewWorker do
+RSpec.describe Gitlab::GithubImport::ImportPullRequestReviewWorker, feature_category: :importers do
it { is_expected.to include_module(Gitlab::GithubImport::ObjectImporter) }
describe '#representation_class' do
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
index 59f45b437c4..864075ca3d7 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportPullRequestWorker do
+RSpec.describe Gitlab::GithubImport::ImportPullRequestWorker, feature_category: :importers do
let(:worker) { described_class.new }
describe '#import' do
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
index 1d32d5c0e21..f4f5353a9cf 100644
--- a/spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_release_attachments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ImportReleaseAttachmentsWorker do
+RSpec.describe Gitlab::GithubImport::ImportReleaseAttachmentsWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb b/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb
index fdcbfb18beb..99ed83ae2db 100644
--- a/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker do
+RSpec.describe Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker, feature_category: :importers do
subject(:worker) { described_class.new }
describe '#import' do
diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
index 3a8b585fa77..abba6cd7734 100644
--- a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::RefreshImportJidWorker do
+RSpec.describe Gitlab::GithubImport::RefreshImportJidWorker, feature_category: :importers do
let(:worker) { described_class.new }
describe '.perform_in_the_future' do
diff --git a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
index 5f60dfc8ca1..e517f30ee2c 100644
--- a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::FinishImportWorker do
+RSpec.describe Gitlab::GithubImport::Stage::FinishImportWorker, feature_category: :importers do
let(:project) { create(:project) }
let(:worker) { described_class.new }
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
index ecfece735af..2945bcbe641 100644
--- a/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_attachments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportAttachmentsWorker, feature_category: :importers do
subject(:worker) { described_class.new }
let_it_be(:project) { create(:project) }
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
index 7b2218b1725..1ad027a007a 100644
--- a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportBaseDataWorker, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:import_state) { create(:import_state, project: project) }
diff --git a/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
new file mode 100644
index 00000000000..0eac9f21b77
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_collaborators_worker_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Stage::ImportCollaboratorsWorker, feature_category: :importers 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::CollaboratorsImporter) }
+ let(:client) { instance_double(Gitlab::GithubImport::Client) }
+
+ describe '#import' do
+ let(:push_rights_granted) { true }
+
+ before do
+ allow(client).to receive(:repository).with(project.import_source)
+ .and_return({ permissions: { push: push_rights_granted } })
+ end
+
+ context 'when user has push access for this repo' do
+ it 'imports all the pull requests' do
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::CollaboratorsImporter)
+ .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 }, :pull_requests_merged_by)
+
+ worker.import(client, project)
+ end
+ end
+
+ context 'when user do not have push access for this repo' do
+ let(:push_rights_granted) { false }
+
+ it 'skips stage' do
+ expect(Gitlab::GithubImport::Importer::CollaboratorsImporter).not_to receive(:new)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, {}, :pull_requests_merged_by)
+
+ worker.import(client, project)
+ end
+ end
+
+ it 'raises an error' do
+ exception = StandardError.new('_some_error_')
+
+ expect_next_instance_of(Gitlab::GithubImport::Importer::CollaboratorsImporter) do |importer|
+ expect(importer).to receive(:execute).and_raise(exception)
+ end
+ expect(Gitlab::Import::ImportFailureService).to receive(:track)
+ .with(
+ project_id: project.id,
+ exception: exception,
+ error_source: described_class.name,
+ fail_import: true,
+ metrics: true
+ ).and_call_original
+
+ expect { worker.import(client, project) }.to raise_error(StandardError)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
index 199d1b9a3ca..c70ee0250e8 100644
--- a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker, feature_category: :importers do
subject(:worker) { described_class.new }
let(:project) { create(:project) }
diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
index beef0864715..872201ece93 100644
--- a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker, feature_category: :importers do
let(:project) { create(:project) }
let(:worker) { described_class.new }
diff --git a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
index 103d55890c4..2449c0505f5 100644
--- a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker, feature_category: :importers do
let(:project) { create(:project) }
let(:worker) { described_class.new }
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 dbcf2083ec1..8c0004c0f3f 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportNotesWorker, feature_category: :importers do
let(:project) { create(:project) }
let(:worker) { described_class.new }
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
index 0770af524a1..f848293a3b2 100644
--- 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:import_state) { create(:import_state, project: project) }
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
index 5d6dcdc10ee..4ffc5a956a3 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker, feature_category: :importers do
let(:project) { create(:project) }
let(:import_state) { create(:import_state, project: project) }
let(:worker) { described_class.new }
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
index 151de9bdffc..41c0b29df7c 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWorker, feature_category: :importers do
subject(:worker) { described_class.new }
let(:project) { instance_double(Project, id: 1, import_state: import_state) }
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
index 18a70273219..c68e84475cb 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker, feature_category: :importers do
let(:project) { create(:project) }
let(:import_state) { create(:import_state, project: project) }
let(:worker) { described_class.new }
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
index b18b5ce64d1..6ebf93730eb 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:import_state) { create(:import_state, project: project) }
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_requests_merged_by)
+ .with(project.id, { '123' => 2 }, :collaborators)
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
index 24fca3b7c73..94d8155d371 100644
--- a/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
+RSpec.describe Gitlab::GithubImport::Stage::ImportRepositoryWorker, feature_category: :importers do
let_it_be(:project) { create(:project, :import_started) }
let(:worker) { described_class.new }
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
:issues, project.import_source, options
).and_return([{ number: 5 }].each)
- expect(Issue).to receive(:track_project_iid!).with(project, 5)
+ expect(Issue).to receive(:track_namespace_iid!).with(project.project_namespace, 5)
expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
.to receive(:perform_async)
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
expect(InternalId).to receive(:exists?).and_return(false)
expect(client).to receive(:each_object).with(:issues, project.import_source, options).and_return([nil].each)
- expect(Issue).not_to receive(:track_project_iid!)
+ expect(Issue).not_to receive(:track_namespace_iid!)
expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
.to receive(:perform_async)
@@ -74,7 +74,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
expect(InternalId).to receive(:exists?).and_return(true)
expect(client).not_to receive(:each_object)
- expect(Issue).not_to receive(:track_project_iid!)
+ expect(Issue).not_to receive(:track_namespace_iid!)
expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
.to receive(:perform_async)
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
expect(InternalId).to receive(:exists?).and_return(false)
expect(client).to receive(:each_object).and_return([nil].each)
- expect(Issue).not_to receive(:track_project_iid!)
+ expect(Issue).not_to receive(:track_namespace_iid!)
expect(Gitlab::Import::ImportFailureService).to receive(:track)
.with(
diff --git a/spec/workers/gitlab/import/stuck_import_job_spec.rb b/spec/workers/gitlab/import/stuck_import_job_spec.rb
index 3a1463e98a0..52721168143 100644
--- a/spec/workers/gitlab/import/stuck_import_job_spec.rb
+++ b/spec/workers/gitlab/import/stuck_import_job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Import::StuckImportJob do
+RSpec.describe Gitlab::Import::StuckImportJob, feature_category: :importers do
let_it_be(:project) { create(:project, :import_started, import_source: 'foo/bar') }
let(:worker) do
diff --git a/spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb b/spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb
index d12d5a605a7..9994896d3c8 100644
--- a/spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb
+++ b/spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do
+RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker, feature_category: :importers do
let(:worker) { described_class.new }
describe 'with scheduled import_status' do
diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
index 0244e69b7b6..5209395923f 100644
--- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::ImportIssueWorker do
+RSpec.describe Gitlab::JiraImport::ImportIssueWorker, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:jira_issue_label_1) { create(:label, project: project) }
diff --git a/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
index 23a764bd972..d1bf71a2095 100644
--- a/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::Stage::FinishImportWorker do
+RSpec.describe Gitlab::JiraImport::Stage::FinishImportWorker, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:worker) { described_class.new }
diff --git a/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
index 28da93b8d00..c594182bf75 100644
--- a/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::Stage::ImportAttachmentsWorker do
+RSpec.describe Gitlab::JiraImport::Stage::ImportAttachmentsWorker, feature_category: :importers do
let_it_be(:project) { create(:project, import_type: 'jira') }
describe 'modules' do
diff --git a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
index 2b08a592164..594f9618770 100644
--- a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker do
+RSpec.describe Gitlab::JiraImport::Stage::ImportIssuesWorker, feature_category: :importers do
include JiraIntegrationHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
index d15f2caba19..d9c2407a423 100644
--- a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
+RSpec.describe Gitlab::JiraImport::Stage::ImportLabelsWorker, feature_category: :importers do
include JiraIntegrationHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
index 2502bbf1df4..41216356033 100644
--- a/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::Stage::ImportNotesWorker do
+RSpec.describe Gitlab::JiraImport::Stage::ImportNotesWorker, feature_category: :importers do
let_it_be(:project) { create(:project, import_type: 'jira') }
describe 'modules' do
diff --git a/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb
index e440884553f..ea0291b1c44 100644
--- a/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JiraImport::Stage::StartImportWorker do
+RSpec.describe Gitlab::JiraImport::Stage::StartImportWorker, feature_category: :importers do
let_it_be(:project) { create(:project, import_type: 'jira') }
let_it_be(:jid) { '12345678' }
diff --git a/spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb b/spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb
index 92754513988..0f9f9b70c3e 100644
--- a/spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do
+RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker, feature_category: :importers do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/workers/gitlab/phabricator_import/base_worker_spec.rb b/spec/workers/gitlab/phabricator_import/base_worker_spec.rb
index 18fa484aa7a..9adba391d0f 100644
--- a/spec/workers/gitlab/phabricator_import/base_worker_spec.rb
+++ b/spec/workers/gitlab/phabricator_import/base_worker_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Gitlab::PhabricatorImport::BaseWorker do
+RSpec.describe Gitlab::PhabricatorImport::BaseWorker, feature_category: :importers do
let(:subclass) do
# Creating an anonymous class for a worker is complicated, as we generate the
# queue name from the class name.
diff --git a/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb b/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb
index 221b6202166..6c1da983640 100644
--- a/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb
+++ b/spec/workers/gitlab/phabricator_import/import_tasks_worker_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Gitlab::PhabricatorImport::ImportTasksWorker do
+RSpec.describe Gitlab::PhabricatorImport::ImportTasksWorker, feature_category: :importers do
describe '#perform' do
it 'calls the correct importer' do
project = create(:project, :import_started, import_url: "https://the.phab.ulr")
diff --git a/spec/workers/gitlab_performance_bar_stats_worker_spec.rb b/spec/workers/gitlab_performance_bar_stats_worker_spec.rb
index 3638add1524..45fb58826ba 100644
--- a/spec/workers/gitlab_performance_bar_stats_worker_spec.rb
+++ b/spec/workers/gitlab_performance_bar_stats_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabPerformanceBarStatsWorker do
+RSpec.describe GitlabPerformanceBarStatsWorker, feature_category: :metrics do
include ExclusiveLeaseHelpers
subject(:worker) { described_class.new }
diff --git a/spec/workers/gitlab_service_ping_worker_spec.rb b/spec/workers/gitlab_service_ping_worker_spec.rb
index f17847a7b33..1d16e8c3496 100644
--- a/spec/workers/gitlab_service_ping_worker_spec.rb
+++ b/spec/workers/gitlab_service_ping_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabServicePingWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe GitlabServicePingWorker, :clean_gitlab_redis_shared_state, feature_category: :service_ping do
let(:payload) { { recorded_at: Time.current.rfc3339 } }
before do
@@ -14,15 +14,13 @@ RSpec.describe GitlabServicePingWorker, :clean_gitlab_redis_shared_state do
allow(subject).to receive(:sleep)
end
- it 'does not run for GitLab.com when triggered from cron' do
- allow(Gitlab).to receive(:com?).and_return(true)
+ it 'does not run for SaaS when triggered from cron', :saas do
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)
+ it 'runs for SaaS when triggered manually', :saas do
expect(ServicePing::SubmitService).to receive(:new)
subject.perform('triggered_from_cron' => false)
diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb
index 838f2ef4ba4..9fff4489667 100644
--- a/spec/workers/gitlab_shell_worker_spec.rb
+++ b/spec/workers/gitlab_shell_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabShellWorker, :sidekiq_inline do
+RSpec.describe GitlabShellWorker, :sidekiq_inline, feature_category: :source_code_management do
describe '#perform' do
Gitlab::Shell::PERMITTED_ACTIONS.each do |action|
describe "with the #{action} action" do
diff --git a/spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb b/spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb
index 5d595a3679b..7aea40807e8 100644
--- a/spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb
+++ b/spec/workers/google_cloud/create_cloudsql_instance_worker_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'google/apis/sqladmin_v1beta4'
-RSpec.describe GoogleCloud::CreateCloudsqlInstanceWorker do
+RSpec.describe GoogleCloud::CreateCloudsqlInstanceWorker, feature_category: :shared do
let(:random_user) { create(:user) }
let(:project) { create(:project) }
let(:worker_options) do
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
index 82ae9010a24..fba4573718a 100644
--- a/spec/workers/group_destroy_worker_spec.rb
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -2,20 +2,29 @@
require 'spec_helper'
-RSpec.describe GroupDestroyWorker do
- let(:group) { create(:group) }
- let!(:project) { create(:project, namespace: group) }
- let(:user) { create(:user) }
+RSpec.describe GroupDestroyWorker, feature_category: :subgroups do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:user) { create(:user) }
before do
group.add_owner(user)
end
- subject { described_class.new }
+ subject(:worker) { described_class.new }
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [group.id, user.id] }
+
+ it 'does not change groups when run twice' do
+ expect { worker.perform(group.id, user.id) }.to change { Group.count }.by(-1)
+ expect { worker.perform(group.id, user.id) }.not_to change { Group.count }
+ end
+ end
describe "#perform" do
- it "deletes the project" do
- subject.perform(group.id, user.id)
+ it "deletes the group and associated projects" do
+ worker.perform(group.id, user.id)
expect(Group.all).not_to include(group)
expect(Project.all).not_to include(project)
diff --git a/spec/workers/group_export_worker_spec.rb b/spec/workers/group_export_worker_spec.rb
index 4e58e3886a4..54f9c38a506 100644
--- a/spec/workers/group_export_worker_spec.rb
+++ b/spec/workers/group_export_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupExportWorker do
+RSpec.describe GroupExportWorker, feature_category: :importers do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
diff --git a/spec/workers/group_import_worker_spec.rb b/spec/workers/group_import_worker_spec.rb
index 5171de7086b..9aa6aaec24c 100644
--- a/spec/workers/group_import_worker_spec.rb
+++ b/spec/workers/group_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupImportWorker do
+RSpec.describe GroupImportWorker, feature_category: :importers do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/workers/groups/update_statistics_worker_spec.rb b/spec/workers/groups/update_statistics_worker_spec.rb
index 7fc166ed300..f47606f0580 100644
--- a/spec/workers/groups/update_statistics_worker_spec.rb
+++ b/spec/workers/groups/update_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UpdateStatisticsWorker do
+RSpec.describe Groups::UpdateStatisticsWorker, feature_category: :source_code_management do
let_it_be(:group) { create(:group) }
let(:statistics) { %w(wiki_size) }
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
index 9d202b9452f..aa1ca698095 100644
--- 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UpdateTwoFactorRequirementForMembersWorker do
+RSpec.describe Groups::UpdateTwoFactorRequirementForMembersWorker, feature_category: :system_access do
let_it_be(:group) { create(:group) }
let(:worker) { described_class.new }
diff --git a/spec/workers/hashed_storage/migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb
index e014297756e..f188928cf92 100644
--- a/spec/workers/hashed_storage/migrator_worker_spec.rb
+++ b/spec/workers/hashed_storage/migrator_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe HashedStorage::MigratorWorker do
+RSpec.describe HashedStorage::MigratorWorker, feature_category: :source_code_management do
subject(:worker) { described_class.new }
let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
diff --git a/spec/workers/hashed_storage/project_migrate_worker_spec.rb b/spec/workers/hashed_storage/project_migrate_worker_spec.rb
index fd460888932..84592e85eaa 100644
--- a/spec/workers/hashed_storage/project_migrate_worker_spec.rb
+++ b/spec/workers/hashed_storage/project_migrate_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe HashedStorage::ProjectMigrateWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe HashedStorage::ProjectMigrateWorker, :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
include ExclusiveLeaseHelpers
let(:migration_service) { ::Projects::HashedStorage::MigrationService }
diff --git a/spec/workers/hashed_storage/project_rollback_worker_spec.rb b/spec/workers/hashed_storage/project_rollback_worker_spec.rb
index fc89ac728b1..f27b5e4b9ce 100644
--- a/spec/workers/hashed_storage/project_rollback_worker_spec.rb
+++ b/spec/workers/hashed_storage/project_rollback_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe HashedStorage::ProjectRollbackWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe HashedStorage::ProjectRollbackWorker, :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
include ExclusiveLeaseHelpers
describe '#perform' do
diff --git a/spec/workers/hashed_storage/rollbacker_worker_spec.rb b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
index 46cca068273..af8957d9b96 100644
--- a/spec/workers/hashed_storage/rollbacker_worker_spec.rb
+++ b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe HashedStorage::RollbackerWorker do
+RSpec.describe HashedStorage::RollbackerWorker, feature_category: :source_code_management do
subject(:worker) { described_class.new }
let(:projects) { create_list(:project, 2, :empty_repo) }
diff --git a/spec/workers/import_issues_csv_worker_spec.rb b/spec/workers/import_issues_csv_worker_spec.rb
index 919ab2b1adf..7f38119d499 100644
--- a/spec/workers/import_issues_csv_worker_spec.rb
+++ b/spec/workers/import_issues_csv_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ImportIssuesCsvWorker do
+RSpec.describe ImportIssuesCsvWorker, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/workers/incident_management/add_severity_system_note_worker_spec.rb b/spec/workers/incident_management/add_severity_system_note_worker_spec.rb
index 4d6e6610a92..cd19c5694b5 100644
--- a/spec/workers/incident_management/add_severity_system_note_worker_spec.rb
+++ b/spec/workers/incident_management/add_severity_system_note_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::AddSeveritySystemNoteWorker do
+RSpec.describe IncidentManagement::AddSeveritySystemNoteWorker, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb
index 145ee780573..bf967a42ceb 100644
--- a/spec/workers/incident_management/close_incident_worker_spec.rb
+++ b/spec/workers/incident_management/close_incident_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::CloseIncidentWorker do
+RSpec.describe IncidentManagement::CloseIncidentWorker, feature_category: :incident_management do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb b/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb
index b81f1a575b5..f537acfaa05 100644
--- a/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb
+++ b/spec/workers/incident_management/pager_duty/process_incident_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::PagerDuty::ProcessIncidentWorker do
+RSpec.describe IncidentManagement::PagerDuty::ProcessIncidentWorker, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:incident_management_setting) { create(:project_incident_management_setting, project: project, pagerduty_active: true) }
diff --git a/spec/workers/incident_management/process_alert_worker_v2_spec.rb b/spec/workers/incident_management/process_alert_worker_v2_spec.rb
index 6cde8b758fa..476b6f04942 100644
--- a/spec/workers/incident_management/process_alert_worker_v2_spec.rb
+++ b/spec/workers/incident_management/process_alert_worker_v2_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IncidentManagement::ProcessAlertWorkerV2 do
+RSpec.describe IncidentManagement::ProcessAlertWorkerV2, feature_category: :incident_management do
let_it_be(:project) { create(:project) }
let_it_be(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) }
diff --git a/spec/workers/integrations/create_external_cross_reference_worker_spec.rb b/spec/workers/integrations/create_external_cross_reference_worker_spec.rb
index 8e586b90905..ee974fe2b28 100644
--- a/spec/workers/integrations/create_external_cross_reference_worker_spec.rb
+++ b/spec/workers/integrations/create_external_cross_reference_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::CreateExternalCrossReferenceWorker do
+RSpec.describe Integrations::CreateExternalCrossReferenceWorker, feature_category: :integrations do
include AfterNextHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/workers/integrations/execute_worker_spec.rb b/spec/workers/integrations/execute_worker_spec.rb
index 0e585e3006c..717e3c65820 100644
--- a/spec/workers/integrations/execute_worker_spec.rb
+++ b/spec/workers/integrations/execute_worker_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Integrations::ExecuteWorker, '#perform' do
+RSpec.describe Integrations::ExecuteWorker, '#perform', feature_category: :integrations do
let_it_be(:integration) { create(:jira_integration) }
let(:worker) { described_class.new }
diff --git a/spec/workers/integrations/irker_worker_spec.rb b/spec/workers/integrations/irker_worker_spec.rb
index 3b7b9af72fd..257a6f72709 100644
--- a/spec/workers/integrations/irker_worker_spec.rb
+++ b/spec/workers/integrations/irker_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::IrkerWorker, '#perform' do
+RSpec.describe Integrations::IrkerWorker, '#perform', feature_category: :integrations do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:push_data) { HashWithIndifferentAccess.new(Gitlab::DataBuilder::Push.build_sample(project, user)) }
diff --git a/spec/workers/invalid_gpg_signature_update_worker_spec.rb b/spec/workers/invalid_gpg_signature_update_worker_spec.rb
index 25c48b55cbb..81a15e79579 100644
--- a/spec/workers/invalid_gpg_signature_update_worker_spec.rb
+++ b/spec/workers/invalid_gpg_signature_update_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe InvalidGpgSignatureUpdateWorker do
+RSpec.describe InvalidGpgSignatureUpdateWorker, feature_category: :source_code_management do
context 'when GpgKey is found' do
it 'calls NotificationService.new.run' do
gpg_key = create(:gpg_key)
diff --git a/spec/workers/issuable/label_links_destroy_worker_spec.rb b/spec/workers/issuable/label_links_destroy_worker_spec.rb
index a838f1c8017..d993c6cb22f 100644
--- a/spec/workers/issuable/label_links_destroy_worker_spec.rb
+++ b/spec/workers/issuable/label_links_destroy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuable::LabelLinksDestroyWorker do
+RSpec.describe Issuable::LabelLinksDestroyWorker, feature_category: :team_planning do
let(:job_args) { [1, 'MergeRequest'] }
let(:service) { double }
diff --git a/spec/workers/issuable_export_csv_worker_spec.rb b/spec/workers/issuable_export_csv_worker_spec.rb
index a5172d916b6..66198157edb 100644
--- a/spec/workers/issuable_export_csv_worker_spec.rb
+++ b/spec/workers/issuable_export_csv_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssuableExportCsvWorker do
+RSpec.describe IssuableExportCsvWorker, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { create(:project, creator: user) }
let(:params) { {} }
@@ -50,6 +50,19 @@ RSpec.describe IssuableExportCsvWorker do
end
end
+ shared_examples 'export with selected fields' do
+ let(:selected_fields) { %w[Title Description'] }
+
+ it 'calls the export service with selected fields' do
+ params[:selected_fields] = selected_fields
+
+ expect(export_service)
+ .to receive(:new).with(anything, project, selected_fields).once.and_call_original
+
+ subject
+ end
+ end
+
context 'when issuable type is MergeRequest' do
let(:issuable_type) { :merge_request }
@@ -58,7 +71,7 @@ RSpec.describe IssuableExportCsvWorker do
end
it 'calls the MR export service' do
- expect(MergeRequests::ExportCsvService).to receive(:new).with(anything, project).once.and_call_original
+ expect(MergeRequests::ExportCsvService).to receive(:new).with(anything, project, []).once.and_call_original
subject
end
@@ -68,6 +81,34 @@ RSpec.describe IssuableExportCsvWorker do
subject
end
+
+ it_behaves_like 'export with selected fields' do
+ let(:export_service) { MergeRequests::ExportCsvService }
+ end
+ end
+
+ context 'for type WorkItem' do
+ let(:issuable_type) { :work_item }
+
+ it 'emails a CSV' do
+ expect { subject }.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+
+ it 'calls the work item export service' do
+ expect(WorkItems::ExportCsvService).to receive(:new).with(anything, project, []).once.and_call_original
+
+ subject
+ end
+
+ it 'calls the WorkItemsFinder' do
+ expect(WorkItems::WorkItemsFinder).to receive(:new).once.and_call_original
+
+ subject
+ end
+
+ it_behaves_like 'export with selected fields' do
+ let(:export_service) { WorkItems::ExportCsvService }
+ end
end
context 'when issuable type is User' do
diff --git a/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb b/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb
index ac430f42e7a..e8f39388328 100644
--- a/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb
+++ b/spec/workers/issuables/clear_groups_issue_counter_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issuables::ClearGroupsIssueCounterWorker do
+RSpec.describe Issuables::ClearGroupsIssueCounterWorker, feature_category: :team_planning do
describe '#perform' do
let_it_be(:user) { create(:user) }
let_it_be(:parent_group) { create(:group) }
diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb
index aecff4a3d93..8988dc86168 100644
--- a/spec/workers/issue_due_scheduler_worker_spec.rb
+++ b/spec/workers/issue_due_scheduler_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssueDueSchedulerWorker do
+RSpec.describe IssueDueSchedulerWorker, feature_category: :team_planning do
describe '#perform' do
it 'schedules one MailScheduler::IssueDueWorker per project with open issues due tomorrow' do
project1 = create(:project)
diff --git a/spec/workers/issues/close_worker_spec.rb b/spec/workers/issues/close_worker_spec.rb
index 3902618ae03..488be7eb067 100644
--- a/spec/workers/issues/close_worker_spec.rb
+++ b/spec/workers/issues/close_worker_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Issues::CloseWorker do
+RSpec.describe Issues::CloseWorker, feature_category: :team_planning do
describe "#perform" do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/workers/issues/placement_worker_spec.rb b/spec/workers/issues/placement_worker_spec.rb
index 33fa0b31b72..710b3d07938 100644
--- a/spec/workers/issues/placement_worker_spec.rb
+++ b/spec/workers/issues/placement_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::PlacementWorker do
+RSpec.describe Issues::PlacementWorker, feature_category: :team_planning do
describe '#perform' do
let_it_be(:time) { Time.now.utc }
let_it_be(:group) { create(:group) }
diff --git a/spec/workers/issues/rebalancing_worker_spec.rb b/spec/workers/issues/rebalancing_worker_spec.rb
index e1c0b348a4f..e0dccf0c56e 100644
--- a/spec/workers/issues/rebalancing_worker_spec.rb
+++ b/spec/workers/issues/rebalancing_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::RebalancingWorker do
+RSpec.describe Issues::RebalancingWorker, feature_category: :team_planning do
describe '#perform' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb b/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb
index 6723c425f34..fa23ffe92f1 100644
--- a/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb
+++ b/spec/workers/issues/reschedule_stuck_issue_rebalances_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::RescheduleStuckIssueRebalancesWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe Issues::RescheduleStuckIssueRebalancesWorker, :clean_gitlab_redis_shared_state, feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/workers/jira_connect/forward_event_worker_spec.rb b/spec/workers/jira_connect/forward_event_worker_spec.rb
index d3db07b8cb4..1be59d846f2 100644
--- a/spec/workers/jira_connect/forward_event_worker_spec.rb
+++ b/spec/workers/jira_connect/forward_event_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::ForwardEventWorker do
+RSpec.describe JiraConnect::ForwardEventWorker, feature_category: :integrations do
describe '#perform' do
let!(:jira_connect_installation) { create(:jira_connect_installation, instance_url: self_managed_url, client_key: client_key, shared_secret: shared_secret) }
let(:base_path) { '/-/jira_connect' }
diff --git a/spec/workers/jira_connect/retry_request_worker_spec.rb b/spec/workers/jira_connect/retry_request_worker_spec.rb
index 7a93e5fe41d..e96a050da13 100644
--- a/spec/workers/jira_connect/retry_request_worker_spec.rb
+++ b/spec/workers/jira_connect/retry_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::RetryRequestWorker do
+RSpec.describe JiraConnect::RetryRequestWorker, feature_category: :integrations do
describe '#perform' do
let(:jwt) { 'some-jwt' }
let(:event_url) { 'https://example.com/somewhere' }
diff --git a/spec/workers/jira_connect/sync_branch_worker_spec.rb b/spec/workers/jira_connect/sync_branch_worker_spec.rb
index 349ccd10694..54b1915b253 100644
--- a/spec/workers/jira_connect/sync_branch_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_branch_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::SyncBranchWorker do
+RSpec.describe JiraConnect::SyncBranchWorker, feature_category: :integrations do
include AfterNextHelpers
it_behaves_like 'worker with data consistency',
diff --git a/spec/workers/jira_connect/sync_builds_worker_spec.rb b/spec/workers/jira_connect/sync_builds_worker_spec.rb
index 9be0cccae2b..6ef15b084a3 100644
--- a/spec/workers/jira_connect/sync_builds_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_builds_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::JiraConnect::SyncBuildsWorker do
+RSpec.describe ::JiraConnect::SyncBuildsWorker, feature_category: :integrations do
include AfterNextHelpers
it_behaves_like 'worker with data consistency',
diff --git a/spec/workers/jira_connect/sync_deployments_worker_spec.rb b/spec/workers/jira_connect/sync_deployments_worker_spec.rb
index 86ba11ebe9c..2e72a94bc1e 100644
--- a/spec/workers/jira_connect/sync_deployments_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_deployments_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::JiraConnect::SyncDeploymentsWorker do
+RSpec.describe ::JiraConnect::SyncDeploymentsWorker, feature_category: :integrations do
include AfterNextHelpers
it_behaves_like 'worker with data consistency',
diff --git a/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb b/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
index 6763aefcbec..c2dbd52398f 100644
--- a/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::JiraConnect::SyncFeatureFlagsWorker do
+RSpec.describe ::JiraConnect::SyncFeatureFlagsWorker, feature_category: :integrations do
include AfterNextHelpers
it_behaves_like 'worker with data consistency',
diff --git a/spec/workers/jira_connect/sync_merge_request_worker_spec.rb b/spec/workers/jira_connect/sync_merge_request_worker_spec.rb
index 65976566b22..23abb915d68 100644
--- a/spec/workers/jira_connect/sync_merge_request_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_merge_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::SyncMergeRequestWorker do
+RSpec.describe JiraConnect::SyncMergeRequestWorker, feature_category: :integrations do
include AfterNextHelpers
it_behaves_like 'worker with data consistency',
diff --git a/spec/workers/jira_connect/sync_project_worker_spec.rb b/spec/workers/jira_connect/sync_project_worker_spec.rb
index d172bde2400..afd56a3b5c1 100644
--- a/spec/workers/jira_connect/sync_project_worker_spec.rb
+++ b/spec/workers/jira_connect/sync_project_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraConnect::SyncProjectWorker, factory_default: :keep do
+RSpec.describe JiraConnect::SyncProjectWorker, factory_default: :keep, feature_category: :integrations do
include AfterNextHelpers
it_behaves_like 'worker with data consistency',
diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
index 09d58a1189e..19860f32b29 100644
--- a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
+++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LooseForeignKeys::CleanupWorker do
+RSpec.describe LooseForeignKeys::CleanupWorker, feature_category: :pods do
include MigrationsHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/workers/mail_scheduler/issue_due_worker_spec.rb b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
index c03cc0bda61..9bdfcd7ca49 100644
--- a/spec/workers/mail_scheduler/issue_due_worker_spec.rb
+++ b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MailScheduler::IssueDueWorker do
+RSpec.describe MailScheduler::IssueDueWorker, feature_category: :team_planning do
describe '#perform' do
let(:worker) { described_class.new }
let(:project) { create(:project) }
diff --git a/spec/workers/mail_scheduler/notification_service_worker_spec.rb b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
index 482d99a43c2..5ba7a0c555f 100644
--- a/spec/workers/mail_scheduler/notification_service_worker_spec.rb
+++ b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MailScheduler::NotificationServiceWorker do
+RSpec.describe MailScheduler::NotificationServiceWorker, feature_category: :team_planning do
let(:worker) { described_class.new }
let(:method) { 'new_key' }
diff --git a/spec/workers/member_invitation_reminder_emails_worker_spec.rb b/spec/workers/member_invitation_reminder_emails_worker_spec.rb
index bb4ff466584..4c6295285ea 100644
--- a/spec/workers/member_invitation_reminder_emails_worker_spec.rb
+++ b/spec/workers/member_invitation_reminder_emails_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MemberInvitationReminderEmailsWorker do
+RSpec.describe MemberInvitationReminderEmailsWorker, feature_category: :subgroups do
describe '#perform' do
subject { described_class.new.perform }
diff --git a/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb b/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb
index 2a325be1225..51fca67ae99 100644
--- a/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb
+++ b/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MembersDestroyer::UnassignIssuablesWorker do
+RSpec.describe MembersDestroyer::UnassignIssuablesWorker, feature_category: :user_management do
let_it_be(:group) { create(:group, :private) }
let_it_be(:user, reload: true) { create(:user) }
diff --git a/spec/workers/merge_request_cleanup_refs_worker_spec.rb b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
index 1de927a81e4..a2df31037be 100644
--- a/spec/workers/merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/merge_request_cleanup_refs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestCleanupRefsWorker do
+RSpec.describe MergeRequestCleanupRefsWorker, feature_category: :code_review_workflow do
let(:worker) { described_class.new }
describe '#perform_work' do
@@ -30,12 +30,12 @@ RSpec.describe MergeRequestCleanupRefsWorker do
expect(cleanup_schedule.completed_at).to be_nil
end
- context "and cleanup schedule has already failed #{described_class::FAILURE_THRESHOLD} times" do
- let(:failed_count) { described_class::FAILURE_THRESHOLD }
+ context "and cleanup schedule has already failed #{WebHooks::AutoDisabling::FAILURE_THRESHOLD} times" do
+ let(:failed_count) { WebHooks::AutoDisabling::FAILURE_THRESHOLD }
it 'marks the cleanup schedule as failed and track the failure' do
expect(cleanup_schedule.reload).to be_failed
- expect(cleanup_schedule.failed_count).to eq(described_class::FAILURE_THRESHOLD + 1)
+ expect(cleanup_schedule.failed_count).to eq(failed_count + 1)
expect(cleanup_schedule.completed_at).to be_nil
end
end
diff --git a/spec/workers/merge_request_mergeability_check_worker_spec.rb b/spec/workers/merge_request_mergeability_check_worker_spec.rb
index 32debcf9651..fd959803006 100644
--- a/spec/workers/merge_request_mergeability_check_worker_spec.rb
+++ b/spec/workers/merge_request_mergeability_check_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestMergeabilityCheckWorker do
+RSpec.describe MergeRequestMergeabilityCheckWorker, feature_category: :code_review_workflow do
subject { described_class.new }
describe '#perform' do
diff --git a/spec/workers/merge_requests/close_issue_worker_spec.rb b/spec/workers/merge_requests/close_issue_worker_spec.rb
index 72fb3be7470..0facd34e790 100644
--- a/spec/workers/merge_requests/close_issue_worker_spec.rb
+++ b/spec/workers/merge_requests/close_issue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CloseIssueWorker do
+RSpec.describe MergeRequests::CloseIssueWorker, feature_category: :code_review_workflow do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/merge_requests/create_approval_event_worker_spec.rb b/spec/workers/merge_requests/create_approval_event_worker_spec.rb
index 8389949ecc9..a9a46420fd5 100644
--- a/spec/workers/merge_requests/create_approval_event_worker_spec.rb
+++ b/spec/workers/merge_requests/create_approval_event_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateApprovalEventWorker do
+RSpec.describe MergeRequests::CreateApprovalEventWorker, feature_category: :code_review_workflow do
let!(:user) { create(:user) }
let!(:project) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/workers/merge_requests/create_approval_note_worker_spec.rb b/spec/workers/merge_requests/create_approval_note_worker_spec.rb
index f58d38599fc..1a9e15c18f0 100644
--- a/spec/workers/merge_requests/create_approval_note_worker_spec.rb
+++ b/spec/workers/merge_requests/create_approval_note_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateApprovalNoteWorker do
+RSpec.describe MergeRequests::CreateApprovalNoteWorker, feature_category: :code_review_workflow do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
index e17ad02e272..d8e49f444a9 100644
--- a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
+++ b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::DeleteSourceBranchWorker do
+RSpec.describe MergeRequests::DeleteSourceBranchWorker, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request, author: user) }
diff --git a/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb b/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb
index 0130ef63f50..13c97f8fe49 100644
--- a/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb
+++ b/spec/workers/merge_requests/execute_approval_hooks_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ExecuteApprovalHooksWorker do
+RSpec.describe MergeRequests::ExecuteApprovalHooksWorker, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/workers/merge_requests/handle_assignees_change_worker_spec.rb b/spec/workers/merge_requests/handle_assignees_change_worker_spec.rb
index 4b45f3562d6..f776c6408ed 100644
--- a/spec/workers/merge_requests/handle_assignees_change_worker_spec.rb
+++ b/spec/workers/merge_requests/handle_assignees_change_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::HandleAssigneesChangeWorker do
+RSpec.describe MergeRequests::HandleAssigneesChangeWorker, feature_category: :code_review_workflow do
include AfterNextHelpers
let_it_be(:merge_request) { create(:merge_request) }
diff --git a/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb b/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb
index f8316a8ff05..39443d48ff6 100644
--- a/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb
+++ b/spec/workers/merge_requests/resolve_todos_after_approval_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ResolveTodosAfterApprovalWorker do
+RSpec.describe MergeRequests::ResolveTodosAfterApprovalWorker, feature_category: :code_review_workflow do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/workers/merge_requests/resolve_todos_worker_spec.rb b/spec/workers/merge_requests/resolve_todos_worker_spec.rb
index 223b8b6803c..1164a1c1ddd 100644
--- a/spec/workers/merge_requests/resolve_todos_worker_spec.rb
+++ b/spec/workers/merge_requests/resolve_todos_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ResolveTodosWorker do
+RSpec.describe MergeRequests::ResolveTodosWorker, feature_category: :code_review_workflow do
include AfterNextHelpers
let_it_be(:merge_request) { create(:merge_request) }
diff --git a/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb b/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb
index 3574b8296a4..912afb59412 100644
--- a/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb
+++ b/spec/workers/merge_requests/update_head_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::UpdateHeadPipelineWorker do
+RSpec.describe MergeRequests::UpdateHeadPipelineWorker, feature_category: :code_review_workflow do
include ProjectForksHelper
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 0268bc2388f..9c6a6564df6 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeWorker do
+RSpec.describe MergeWorker, feature_category: :source_code_management do
describe "remove source branch" do
let!(:merge_request) { create(:merge_request, source_branch: "markdown") }
let!(:source_project) { merge_request.source_project }
diff --git a/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb b/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb
index 491ea64cff1..c7e2bbc2ad9 100644
--- a/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb
+++ b/spec/workers/metrics/dashboard/prune_old_annotations_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::PruneOldAnnotationsWorker do
+RSpec.describe Metrics::Dashboard::PruneOldAnnotationsWorker, feature_category: :metrics do
let_it_be(:now) { DateTime.parse('2020-06-02T00:12:00Z') }
let_it_be(:two_weeks_old_annotation) { create(:metrics_dashboard_annotation, starting_at: now.advance(weeks: -2)) }
let_it_be(:one_day_old_annotation) { create(:metrics_dashboard_annotation, starting_at: now.advance(days: -1)) }
diff --git a/spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb b/spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb
index e0a5a8fd448..75866a4eca2 100644
--- a/spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb
+++ b/spec/workers/metrics/dashboard/schedule_annotations_prune_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::ScheduleAnnotationsPruneWorker do
+RSpec.describe Metrics::Dashboard::ScheduleAnnotationsPruneWorker, feature_category: :metrics do
describe '#perform' do
it 'schedules annotations prune job with default cut off date' do
expect(Metrics::Dashboard::PruneOldAnnotationsWorker).to receive(:perform_async)
diff --git a/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb b/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
index 4b670a753e7..f7d67b2064e 100644
--- a/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
+++ b/spec/workers/metrics/dashboard/sync_dashboards_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Metrics::Dashboard::SyncDashboardsWorker do
+RSpec.describe Metrics::Dashboard::SyncDashboardsWorker, feature_category: :metrics do
include MetricsDashboardHelpers
subject(:worker) { described_class.new }
diff --git a/spec/workers/migrate_external_diffs_worker_spec.rb b/spec/workers/migrate_external_diffs_worker_spec.rb
index 36669b4e694..cb5e3ff3651 100644
--- a/spec/workers/migrate_external_diffs_worker_spec.rb
+++ b/spec/workers/migrate_external_diffs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MigrateExternalDiffsWorker do
+RSpec.describe MigrateExternalDiffsWorker, feature_category: :code_review_workflow do
let(:worker) { described_class.new }
let(:diff) { create(:merge_request).merge_request_diff }
diff --git a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
index 2e7b6356692..237b5081bb1 100644
--- a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
+++ b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform', unless: Gitlab.ee? do
+RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform', unless: Gitlab.ee?, feature_category: :experimentation_activation do
# Running this in EE would call the overridden method, which can't be tested in CE.
# The EE code is covered in a separate EE spec.
diff --git a/spec/workers/namespaces/process_sync_events_worker_spec.rb b/spec/workers/namespaces/process_sync_events_worker_spec.rb
index 9f389089609..efa0053c145 100644
--- a/spec/workers/namespaces/process_sync_events_worker_spec.rb
+++ b/spec/workers/namespaces/process_sync_events_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::ProcessSyncEventsWorker do
+RSpec.describe Namespaces::ProcessSyncEventsWorker, feature_category: :pods do
let!(:group1) { create(:group) }
let!(:group2) { create(:group) }
let!(:group3) { create(:group) }
diff --git a/spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb b/spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb
index d8c60932d92..02856e16552 100644
--- a/spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb
+++ b/spec/workers/namespaces/prune_aggregation_schedules_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::PruneAggregationSchedulesWorker, '#perform', :clean_gitlab_redis_shared_state do
+RSpec.describe Namespaces::PruneAggregationSchedulesWorker, '#perform', :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
include ExclusiveLeaseHelpers
let(:namespaces) { create_list(:namespace, 5, :with_aggregation_schedule) }
diff --git a/spec/workers/namespaces/root_statistics_worker_spec.rb b/spec/workers/namespaces/root_statistics_worker_spec.rb
index e047c94816f..8409fffca26 100644
--- a/spec/workers/namespaces/root_statistics_worker_spec.rb
+++ b/spec/workers/namespaces/root_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
+RSpec.describe Namespaces::RootStatisticsWorker, '#perform', feature_category: :source_code_management do
let(:group) { create(:group, :with_aggregation_schedule) }
subject(:worker) { described_class.new }
diff --git a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
index 62f9be501cc..69bd0f1ce47 100644
--- a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
+++ b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_redis_shared_state do
+RSpec.describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
let(:group) { create(:group) }
subject(:worker) { described_class.new }
diff --git a/spec/workers/namespaces/update_root_statistics_worker_spec.rb b/spec/workers/namespaces/update_root_statistics_worker_spec.rb
index f2f633a39ca..85fd68094ce 100644
--- a/spec/workers/namespaces/update_root_statistics_worker_spec.rb
+++ b/spec/workers/namespaces/update_root_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespaces::UpdateRootStatisticsWorker do
+RSpec.describe Namespaces::UpdateRootStatisticsWorker, feature_category: :source_code_management do
let(:namespace_id) { 123 }
let(:event) do
diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb
index b9053b10419..540296374ef 100644
--- a/spec/workers/new_issue_worker_spec.rb
+++ b/spec/workers/new_issue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NewIssueWorker do
+RSpec.describe NewIssueWorker, feature_category: :team_planning do
include AfterNextHelpers
describe '#perform' do
diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb
index a8e1c3f4bf1..58f6792f9a0 100644
--- a/spec/workers/new_merge_request_worker_spec.rb
+++ b/spec/workers/new_merge_request_worker_spec.rb
@@ -107,18 +107,6 @@ RSpec.describe NewMergeRequestWorker, feature_category: :code_review_workflow do
stub_feature_flags(add_prepared_state_to_mr: true)
end
- context 'when the merge request is prepared' do
- before do
- merge_request.update!(prepared_at: Time.current)
- end
-
- it 'does not call the create service' do
- expect(MergeRequests::AfterCreateService).not_to receive(:new)
-
- worker.perform(merge_request.id, user.id)
- end
- end
-
context 'when the merge request is not prepared' do
it 'calls the create service' do
expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb
index 7ba3fe94254..651b5742854 100644
--- a/spec/workers/new_note_worker_spec.rb
+++ b/spec/workers/new_note_worker_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe NewNoteWorker do
+RSpec.describe NewNoteWorker, feature_category: :team_planning do
context 'when Note found' do
let(:note) { create(:note) }
diff --git a/spec/workers/object_pool/create_worker_spec.rb b/spec/workers/object_pool/create_worker_spec.rb
index 4ec409bdf47..573cb3413f5 100644
--- a/spec/workers/object_pool/create_worker_spec.rb
+++ b/spec/workers/object_pool/create_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ObjectPool::CreateWorker do
+RSpec.describe ObjectPool::CreateWorker, feature_category: :shared do
let(:pool) { create(:pool_repository, :scheduled) }
subject { described_class.new }
diff --git a/spec/workers/object_pool/destroy_worker_spec.rb b/spec/workers/object_pool/destroy_worker_spec.rb
index 130a666a42e..f83d3814c63 100644
--- a/spec/workers/object_pool/destroy_worker_spec.rb
+++ b/spec/workers/object_pool/destroy_worker_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.describe ObjectPool::DestroyWorker do
+RSpec.describe ObjectPool::DestroyWorker, feature_category: :shared do
describe '#perform' do
context 'when no pool is in the database' do
it "doesn't raise an error" do
diff --git a/spec/workers/object_pool/join_worker_spec.rb b/spec/workers/object_pool/join_worker_spec.rb
index 335c45e14e0..e0173cad53b 100644
--- a/spec/workers/object_pool/join_worker_spec.rb
+++ b/spec/workers/object_pool/join_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ObjectPool::JoinWorker do
+RSpec.describe ObjectPool::JoinWorker, feature_category: :shared do
let(:pool) { create(:pool_repository, :ready) }
let(:project) { pool.source_project }
let(:repository) { project.repository }
diff --git a/spec/workers/onboarding/issue_created_worker_spec.rb b/spec/workers/onboarding/issue_created_worker_spec.rb
index 70a0156d444..d12f51ceff6 100644
--- a/spec/workers/onboarding/issue_created_worker_spec.rb
+++ b/spec/workers/onboarding/issue_created_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Onboarding::IssueCreatedWorker, '#perform' do
+RSpec.describe Onboarding::IssueCreatedWorker, '#perform', feature_category: :onboarding do
let_it_be(:issue) { create(:issue) }
let(:namespace) { issue.project.namespace }
diff --git a/spec/workers/onboarding/pipeline_created_worker_spec.rb b/spec/workers/onboarding/pipeline_created_worker_spec.rb
index 75bdea28eef..e6dc0f9b689 100644
--- a/spec/workers/onboarding/pipeline_created_worker_spec.rb
+++ b/spec/workers/onboarding/pipeline_created_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Onboarding::PipelineCreatedWorker, '#perform' do
+RSpec.describe Onboarding::PipelineCreatedWorker, '#perform', feature_category: :onboarding do
let_it_be(:ci_pipeline) { create(:ci_pipeline) }
it_behaves_like 'records an onboarding progress action', :pipeline_created do
diff --git a/spec/workers/onboarding/progress_worker_spec.rb b/spec/workers/onboarding/progress_worker_spec.rb
index bbf4875069e..da760c23367 100644
--- a/spec/workers/onboarding/progress_worker_spec.rb
+++ b/spec/workers/onboarding/progress_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Onboarding::ProgressWorker, '#perform' do
+RSpec.describe Onboarding::ProgressWorker, '#perform', feature_category: :onboarding do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:action) { 'git_pull' }
diff --git a/spec/workers/onboarding/user_added_worker_spec.rb b/spec/workers/onboarding/user_added_worker_spec.rb
index 6dbd875c93b..d32bd5ee19c 100644
--- a/spec/workers/onboarding/user_added_worker_spec.rb
+++ b/spec/workers/onboarding/user_added_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Onboarding::UserAddedWorker, '#perform' do
+RSpec.describe Onboarding::UserAddedWorker, '#perform', feature_category: :onboarding do
let_it_be(:namespace) { create(:group) }
subject { described_class.new.perform(namespace.id) }
diff --git a/spec/workers/packages/cleanup/execute_policy_worker_spec.rb b/spec/workers/packages/cleanup/execute_policy_worker_spec.rb
index 6325a82ed3d..fc3ed1f3a05 100644
--- a/spec/workers/packages/cleanup/execute_policy_worker_spec.rb
+++ b/spec/workers/packages/cleanup/execute_policy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Cleanup::ExecutePolicyWorker do
+RSpec.describe Packages::Cleanup::ExecutePolicyWorker, feature_category: :package_registry do
let(:worker) { described_class.new }
describe '#perform_work' do
diff --git a/spec/workers/packages/cleanup_package_file_worker_spec.rb b/spec/workers/packages/cleanup_package_file_worker_spec.rb
index 95cf65c18c5..6e42565abbc 100644
--- a/spec/workers/packages/cleanup_package_file_worker_spec.rb
+++ b/spec/workers/packages/cleanup_package_file_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::CleanupPackageFileWorker do
+RSpec.describe Packages::CleanupPackageFileWorker, feature_category: :package_registry do
let_it_be_with_reload(:package) { create(:package) }
let(:worker) { described_class.new }
diff --git a/spec/workers/packages/cleanup_package_registry_worker_spec.rb b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
index e12f2198f66..f70103070ef 100644
--- a/spec/workers/packages/cleanup_package_registry_worker_spec.rb
+++ b/spec/workers/packages/cleanup_package_registry_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::CleanupPackageRegistryWorker do
+RSpec.describe Packages::CleanupPackageRegistryWorker, feature_category: :package_registry do
describe '#perform' do
let_it_be_with_reload(:package_files) { create_list(:package_file, 2, :pending_destruction) }
let_it_be(:policy) { create(:packages_cleanup_policy, :runnable) }
diff --git a/spec/workers/packages/composer/cache_cleanup_worker_spec.rb b/spec/workers/packages/composer/cache_cleanup_worker_spec.rb
index 39eac4e4ae1..67d4ce36437 100644
--- a/spec/workers/packages/composer/cache_cleanup_worker_spec.rb
+++ b/spec/workers/packages/composer/cache_cleanup_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Composer::CacheCleanupWorker, type: :worker do
+RSpec.describe Packages::Composer::CacheCleanupWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
let_it_be(:group) { create(:group) }
diff --git a/spec/workers/packages/composer/cache_update_worker_spec.rb b/spec/workers/packages/composer/cache_update_worker_spec.rb
index 6c17d49e986..cc3047895d4 100644
--- a/spec/workers/packages/composer/cache_update_worker_spec.rb
+++ b/spec/workers/packages/composer/cache_update_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Composer::CacheUpdateWorker, type: :worker do
+RSpec.describe Packages::Composer::CacheUpdateWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
let_it_be(:package_name) { 'sample-project' }
let_it_be(:json) { { 'name' => package_name } }
diff --git a/spec/workers/packages/debian/generate_distribution_worker_spec.rb b/spec/workers/packages/debian/generate_distribution_worker_spec.rb
index c4e974ec8eb..acdfcc5275f 100644
--- a/spec/workers/packages/debian/generate_distribution_worker_spec.rb
+++ b/spec/workers/packages/debian/generate_distribution_worker_spec.rb
@@ -4,13 +4,13 @@ require 'spec_helper'
RSpec.describe Packages::Debian::GenerateDistributionWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
- let(:container_type) { distribution.container_type }
+ let(:container_type_as_string) { container_type.to_s }
let(:distribution_id) { distribution.id }
- subject { described_class.new.perform(container_type, distribution_id) }
+ subject { described_class.new.perform(container_type_as_string, distribution_id) }
- let(:subject2) { described_class.new.perform(container_type, distribution_id) }
- let(:subject3) { described_class.new.perform(container_type, distribution_id) }
+ let(:subject2) { described_class.new.perform(container_type_as_string, distribution_id) }
+ let(:subject3) { described_class.new.perform(container_type_as_string, distribution_id) }
include_context 'with published Debian package'
@@ -54,7 +54,7 @@ RSpec.describe Packages::Debian::GenerateDistributionWorker, type: :worker, feat
context 'with valid parameters' do
it_behaves_like 'an idempotent worker' do
- let(:job_args) { [container_type, distribution_id] }
+ let(:job_args) { [container_type_as_string, distribution_id] }
it_behaves_like 'Generate Debian Distribution and component files'
end
diff --git a/spec/workers/packages/debian/process_changes_worker_spec.rb b/spec/workers/packages/debian/process_changes_worker_spec.rb
index b96b75e93b9..ddd608e768c 100644
--- a/spec/workers/packages/debian/process_changes_worker_spec.rb
+++ b/spec/workers/packages/debian/process_changes_worker_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker, feature_ca
expect { subject }
.to not_change { Packages::Package.count }
.and change { Packages::PackageFile.count }.by(-1)
- .and change { incoming.package_files.count }.from(7).to(6)
+ .and change { incoming.package_files.count }.from(8).to(7)
end
end
@@ -104,7 +104,7 @@ RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker, feature_ca
expect { subject }
.to not_change { Packages::Package.count }
.and change { Packages::PackageFile.count }.by(-1)
- .and change { incoming.package_files.count }.from(7).to(6)
+ .and change { incoming.package_files.count }.from(8).to(7)
expect { package_file.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
@@ -120,7 +120,7 @@ RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker, feature_ca
expect { subject }
.to change { Packages::Package.count }.from(1).to(2)
.and not_change { Packages::PackageFile.count }
- .and change { incoming.package_files.count }.from(7).to(0)
+ .and change { incoming.package_files.count }.from(8).to(0)
.and change { package_file&.debian_file_metadatum&.reload&.file_type }.from('unknown').to('changes')
created_package = Packages::Package.last
diff --git a/spec/workers/packages/debian/process_package_file_worker_spec.rb b/spec/workers/packages/debian/process_package_file_worker_spec.rb
index 239ee8e1035..44769ec6a14 100644
--- a/spec/workers/packages/debian/process_package_file_worker_spec.rb
+++ b/spec/workers/packages/debian/process_package_file_worker_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Packages::Debian::ProcessPackageFileWorker, type: :worker, featur
where(:case_name, :expected_file_type, :file_name, :component_name) do
'with a deb' | 'deb' | 'libsample0_1.2.3~alpha2_amd64.deb' | 'main'
'with an udeb' | 'udeb' | 'sample-udeb_1.2.3~alpha2_amd64.udeb' | 'contrib'
+ 'with a ddeb' | 'ddeb' | 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | 'main'
end
with_them do
diff --git a/spec/workers/packages/go/sync_packages_worker_spec.rb b/spec/workers/packages/go/sync_packages_worker_spec.rb
index 5eeef1f7c08..5fdb7a242f6 100644
--- a/spec/workers/packages/go/sync_packages_worker_spec.rb
+++ b/spec/workers/packages/go/sync_packages_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Go::SyncPackagesWorker, type: :worker do
+RSpec.describe Packages::Go::SyncPackagesWorker, type: :worker, feature_category: :package_registry do
include_context 'basic Go module'
before do
diff --git a/spec/workers/packages/helm/extraction_worker_spec.rb b/spec/workers/packages/helm/extraction_worker_spec.rb
index 70a090d6989..a764c2ad939 100644
--- a/spec/workers/packages/helm/extraction_worker_spec.rb
+++ b/spec/workers/packages/helm/extraction_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Helm::ExtractionWorker, type: :worker do
+RSpec.describe Packages::Helm::ExtractionWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
let_it_be(:package) { create(:helm_package, without_package_files: true, status: 'processing') }
diff --git a/spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb b/spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb
index 15d9e4c347b..29fbf17d49a 100644
--- a/spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb
+++ b/spec/workers/packages/mark_package_files_for_destruction_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::MarkPackageFilesForDestructionWorker, :aggregate_failures do
+RSpec.describe Packages::MarkPackageFilesForDestructionWorker, :aggregate_failures, feature_category: :package_registry do
describe '#perform' do
let_it_be(:package) { create(:package) }
let_it_be(:package_files) { create_list(:package_file, 3, package: package) }
diff --git a/spec/workers/packages/nuget/extraction_worker_spec.rb b/spec/workers/packages/nuget/extraction_worker_spec.rb
index 5186c037dc5..d6cc2315fb3 100644
--- a/spec/workers/packages/nuget/extraction_worker_spec.rb
+++ b/spec/workers/packages/nuget/extraction_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do
+RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
let!(:package) { create(:nuget_package) }
let(:package_file) { package.package_files.first }
diff --git a/spec/workers/packages/rubygems/extraction_worker_spec.rb b/spec/workers/packages/rubygems/extraction_worker_spec.rb
index 0e67f3ac62e..8ad4c2e6447 100644
--- a/spec/workers/packages/rubygems/extraction_worker_spec.rb
+++ b/spec/workers/packages/rubygems/extraction_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker do
+RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
let_it_be(:package) { create(:rubygems_package, :processing) }
diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb
index f152d019de6..eb9f87bd831 100644
--- a/spec/workers/pages_domain_removal_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomainRemovalCronWorker do
+RSpec.describe PagesDomainRemovalCronWorker, feature_category: :pages do
subject(:worker) { described_class.new }
describe '#perform' do
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 70ffef5342e..5711b7787ea 100644
--- a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomainSslRenewalCronWorker do
+RSpec.describe PagesDomainSslRenewalCronWorker, feature_category: :pages do
include LetsEncryptHelpers
subject(:worker) { described_class.new }
diff --git a/spec/workers/pages_domain_ssl_renewal_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_worker_spec.rb
index f8149b23a08..daaa939ecfe 100644
--- a/spec/workers/pages_domain_ssl_renewal_worker_spec.rb
+++ b/spec/workers/pages_domain_ssl_renewal_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomainSslRenewalWorker do
+RSpec.describe PagesDomainSslRenewalWorker, feature_category: :pages do
include LetsEncryptHelpers
subject(:worker) { described_class.new }
diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb
index 01eaf984c90..be30e45fc94 100644
--- a/spec/workers/pages_domain_verification_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomainVerificationCronWorker do
+RSpec.describe PagesDomainVerificationCronWorker, feature_category: :pages do
subject(:worker) { described_class.new }
describe '#perform', :sidekiq do
diff --git a/spec/workers/pages_domain_verification_worker_spec.rb b/spec/workers/pages_domain_verification_worker_spec.rb
index 6d2f9ee2f8d..08f383c954f 100644
--- a/spec/workers/pages_domain_verification_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesDomainVerificationWorker do
+RSpec.describe PagesDomainVerificationWorker, feature_category: :pages do
subject(:worker) { described_class.new }
let(:domain) { create(:pages_domain) }
diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb
index f0d29037fa4..74da4707205 100644
--- a/spec/workers/pages_worker_spec.rb
+++ b/spec/workers/pages_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PagesWorker, :sidekiq_inline do
+RSpec.describe PagesWorker, :sidekiq_inline, feature_category: :pages do
let_it_be(:ci_build) { create(:ci_build) }
context 'when called with the deploy action' do
diff --git a/spec/workers/partition_creation_worker_spec.rb b/spec/workers/partition_creation_worker_spec.rb
index 5d15870b7f6..ab525fd5ce2 100644
--- a/spec/workers/partition_creation_worker_spec.rb
+++ b/spec/workers/partition_creation_worker_spec.rb
@@ -2,7 +2,7 @@
#
require 'spec_helper'
-RSpec.describe PartitionCreationWorker do
+RSpec.describe PartitionCreationWorker, feature_category: :database do
subject { described_class.new.perform }
let(:management_worker) { double }
diff --git a/spec/workers/personal_access_tokens/expired_notification_worker_spec.rb b/spec/workers/personal_access_tokens/expired_notification_worker_spec.rb
index 7c3c48b3f80..7a3491b49d6 100644
--- a/spec/workers/personal_access_tokens/expired_notification_worker_spec.rb
+++ b/spec/workers/personal_access_tokens/expired_notification_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokens::ExpiredNotificationWorker, type: :worker do
+RSpec.describe PersonalAccessTokens::ExpiredNotificationWorker, type: :worker, feature_category: :system_access do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/personal_access_tokens/expiring_worker_spec.rb b/spec/workers/personal_access_tokens/expiring_worker_spec.rb
index 7fa777b911a..01ce4e85fe2 100644
--- a/spec/workers/personal_access_tokens/expiring_worker_spec.rb
+++ b/spec/workers/personal_access_tokens/expiring_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
+RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_category: :system_access do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb
index 5d28b1e129a..a8b0f91bf7d 100644
--- a/spec/workers/pipeline_hooks_worker_spec.rb
+++ b/spec/workers/pipeline_hooks_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PipelineHooksWorker do
+RSpec.describe PipelineHooksWorker, feature_category: :continuous_integration do
describe '#perform' do
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) }
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index c73b84e26a6..f7b397d91a6 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PipelineMetricsWorker do
+RSpec.describe PipelineMetricsWorker, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let!(:merge_request) do
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
index 672debd0501..05d0186e873 100644
--- a/spec/workers/pipeline_notification_worker_spec.rb
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PipelineNotificationWorker, :mailer do
+RSpec.describe PipelineNotificationWorker, :mailer, feature_category: :continuous_integration do
let_it_be(:pipeline) { create(:ci_pipeline) }
describe '#execute' do
diff --git a/spec/workers/pipeline_process_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb
index 6e95b7a4753..6c6851c51ce 100644
--- a/spec/workers/pipeline_process_worker_spec.rb
+++ b/spec/workers/pipeline_process_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PipelineProcessWorker do
+RSpec.describe PipelineProcessWorker, feature_category: :continuous_integration do
let_it_be(:pipeline) { create(:ci_pipeline) }
include_examples 'an idempotent worker' do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 210987555c9..bd1bfc46d53 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PostReceive do
+RSpec.describe PostReceive, feature_category: :source_code_management do
include AfterNextHelpers
let(:changes) do
@@ -280,7 +280,6 @@ RSpec.describe PostReceive do
let(:category) { described_class.name }
let(:namespace) { project.namespace }
let(:user) { project.creator }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:label) { 'counts.source_code_pushes' }
let(:property) { 'source_code_pushes' }
let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: label).to_h] }
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 143809e8f2a..1fc77c42cbc 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProcessCommitWorker do
+RSpec.describe ProcessCommitWorker, feature_category: :source_code_management do
let(:worker) { described_class.new }
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 3c807ef9ffd..508dacea2fb 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectCacheWorker do
+RSpec.describe ProjectCacheWorker, feature_category: :source_code_management do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 25508928bbf..d699393d7a0 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -2,15 +2,26 @@
require 'spec_helper'
-RSpec.describe ProjectDestroyWorker do
- let(:project) { create(:project, :repository, pending_delete: true) }
- let!(:repository) { project.repository.raw }
+RSpec.describe ProjectDestroyWorker, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :repository, pending_delete: true) }
+ let_it_be(:repository) { project.repository.raw }
- subject { described_class.new }
+ let(:user) { project.first_owner }
+
+ subject(:worker) { described_class.new }
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [project.id, user.id, {}] }
+
+ it 'does not change projects when run twice' do
+ expect { worker.perform(project.id, user.id, {}) }.to change { Project.count }.by(-1)
+ expect { worker.perform(project.id, user.id, {}) }.not_to change { Project.count }
+ end
+ end
describe '#perform' do
it 'deletes the project' do
- subject.perform(project.id, project.first_owner.id, {})
+ worker.perform(project.id, user.id, {})
expect(Project.all).not_to include(project)
expect(repository).not_to exist
@@ -18,13 +29,13 @@ RSpec.describe ProjectDestroyWorker do
it 'does not raise error when project could not be found' do
expect do
- subject.perform(-1, project.first_owner.id, {})
+ worker.perform(-1, user.id, {})
end.not_to raise_error
end
it 'does not raise error when user could not be found' do
expect do
- subject.perform(project.id, -1, {})
+ worker.perform(project.id, -1, {})
end.not_to raise_error
end
end
diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb
index dd0a921059d..eaf1536da63 100644
--- a/spec/workers/project_export_worker_spec.rb
+++ b/spec/workers/project_export_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectExportWorker do
+RSpec.describe ProjectExportWorker, feature_category: :importers do
it_behaves_like 'export worker'
context 'exporters duration measuring' do
diff --git a/spec/workers/projects/after_import_worker_spec.rb b/spec/workers/projects/after_import_worker_spec.rb
index 85d15c89b0a..5af4f49d6e0 100644
--- a/spec/workers/projects/after_import_worker_spec.rb
+++ b/spec/workers/projects/after_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::AfterImportWorker do
+RSpec.describe Projects::AfterImportWorker, feature_category: :importers do
subject { worker.perform(project.id) }
let(:worker) { described_class.new }
diff --git a/spec/workers/projects/finalize_project_statistics_refresh_worker_spec.rb b/spec/workers/projects/finalize_project_statistics_refresh_worker_spec.rb
index 932ba29f806..1379b6785eb 100644
--- a/spec/workers/projects/finalize_project_statistics_refresh_worker_spec.rb
+++ b/spec/workers/projects/finalize_project_statistics_refresh_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::FinalizeProjectStatisticsRefreshWorker do
+RSpec.describe Projects::FinalizeProjectStatisticsRefreshWorker, feature_category: :projects do
let_it_be(:record) { create(:project_build_artifacts_size_refresh, :finalizing) }
describe '#perform' do
diff --git a/spec/workers/projects/import_export/create_relation_exports_worker_spec.rb b/spec/workers/projects/import_export/create_relation_exports_worker_spec.rb
new file mode 100644
index 00000000000..2ff91150fda
--- /dev/null
+++ b/spec/workers/projects/import_export/create_relation_exports_worker_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::CreateRelationExportsWorker, feature_category: :importers do
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:after_export_strategy) { {} }
+ let(:job_args) { [user.id, project.id, after_export_strategy] }
+
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid) { SecureRandom.hex(8) }
+ end
+ end
+
+ it_behaves_like 'an idempotent worker'
+
+ context 'when job is re-enqueued after an interuption and same JID is used' do
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid).and_return(1234)
+ end
+ end
+
+ it_behaves_like 'an idempotent worker'
+
+ it 'does not start the export process twice' do
+ project.export_jobs.create!(jid: 1234, status_event: :start)
+
+ expect { described_class.new.perform(user.id, project.id, after_export_strategy) }
+ .to change { Projects::ImportExport::WaitRelationExportsWorker.jobs.size }.by(0)
+ end
+ end
+
+ it 'creates a export_job and sets the status to `started`' do
+ described_class.new.perform(user.id, project.id, after_export_strategy)
+
+ export_job = project.export_jobs.last
+ expect(export_job.started?).to eq(true)
+ end
+
+ it 'creates relation export records and enqueues a worker for each relation to be exported' do
+ allow(Projects::ImportExport::RelationExport).to receive(:relation_names_list).and_return(%w[relation_1 relation_2])
+
+ expect { described_class.new.perform(user.id, project.id, after_export_strategy) }
+ .to change { Projects::ImportExport::RelationExportWorker.jobs.size }.by(2)
+
+ relation_exports = project.export_jobs.last.relation_exports
+ expect(relation_exports.collect(&:relation)).to match_array(%w[relation_1 relation_2])
+ end
+
+ it 'enqueues a WaitRelationExportsWorker' do
+ allow(Projects::ImportExport::WaitRelationExportsWorker).to receive(:perform_in)
+
+ described_class.new.perform(user.id, project.id, after_export_strategy)
+
+ export_job = project.export_jobs.last
+ expect(Projects::ImportExport::WaitRelationExportsWorker).to have_received(:perform_in).with(
+ described_class::INITIAL_DELAY,
+ export_job.id,
+ user.id,
+ after_export_strategy
+ )
+ end
+end
diff --git a/spec/workers/projects/import_export/relation_export_worker_spec.rb b/spec/workers/projects/import_export/relation_export_worker_spec.rb
index 236650fe55b..16ee73040b1 100644
--- a/spec/workers/projects/import_export/relation_export_worker_spec.rb
+++ b/spec/workers/projects/import_export/relation_export_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker do
+RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker, feature_category: :importers do
let(:project_relation_export) { create(:project_relation_export) }
let(:job_args) { [project_relation_export.id] }
@@ -11,26 +11,61 @@ RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker do
describe '#perform' do
subject(:worker) { described_class.new }
- context 'when relation export has initial state queued' do
- let(:project_relation_export) { create(:project_relation_export) }
+ context 'when relation export has initial status `queued`' do
+ it 'exports the relation' do
+ expect_next_instance_of(Projects::ImportExport::RelationExportService) do |service|
+ expect(service).to receive(:execute)
+ end
- it 'calls RelationExportService' do
+ worker.perform(project_relation_export.id)
+ end
+ end
+
+ context 'when relation export has status `started`' do
+ let(:project_relation_export) { create(:project_relation_export, :started) }
+
+ it 'retries the export of the relation' do
expect_next_instance_of(Projects::ImportExport::RelationExportService) do |service|
expect(service).to receive(:execute)
end
worker.perform(project_relation_export.id)
+
+ expect(project_relation_export.reload.queued?).to eq(true)
end
end
- context 'when relation export does not have queued state' do
- let(:project_relation_export) { create(:project_relation_export, status_event: :start) }
+ context 'when relation export does not have status `queued` or `started`' do
+ let(:project_relation_export) { create(:project_relation_export, :finished) }
- it 'does not call RelationExportService' do
+ it 'does not export the relation' do
expect(Projects::ImportExport::RelationExportService).not_to receive(:new)
worker.perform(project_relation_export.id)
end
end
end
+
+ describe '.sidekiq_retries_exhausted' do
+ let(:job) { { 'args' => [project_relation_export.id], 'error_message' => 'Error message' } }
+
+ it 'sets relation export status to `failed`' do
+ described_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(project_relation_export.reload.failed?).to eq(true)
+ end
+
+ it 'logs the error message' do
+ expect_next_instance_of(Gitlab::Export::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ hash_including(
+ message: 'Project relation export failed',
+ export_error: 'Error message'
+ )
+ )
+ end
+
+ described_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
end
diff --git a/spec/workers/projects/import_export/wait_relation_exports_worker_spec.rb b/spec/workers/projects/import_export/wait_relation_exports_worker_spec.rb
new file mode 100644
index 00000000000..52394b8998e
--- /dev/null
+++ b/spec/workers/projects/import_export/wait_relation_exports_worker_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::WaitRelationExportsWorker, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project_export_job) { create(:project_export_job, :started) }
+
+ let(:after_export_strategy) { {} }
+ let(:job_args) { [project_export_job.id, user.id, after_export_strategy] }
+
+ def create_relation_export(trait, relation, export_error = nil)
+ create(:project_relation_export, trait,
+ { project_export_job: project_export_job, relation: relation, export_error: export_error }
+ )
+ end
+
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid) { SecureRandom.hex(8) }
+ end
+ end
+
+ context 'when export job status is not `started`' do
+ it 'does not perform any operation and finishes the worker' do
+ finished_export_job = create(:project_export_job, :finished)
+
+ expect { described_class.new.perform(finished_export_job.id, user.id, after_export_strategy) }
+ .to change { Projects::ImportExport::ParallelProjectExportWorker.jobs.size }.by(0)
+ .and change { described_class.jobs.size }.by(0)
+ end
+ end
+
+ context 'when there are relation exports with status `queued`' do
+ before do
+ create_relation_export(:finished, 'labels')
+ create_relation_export(:started, 'milestones')
+ create_relation_export(:queued, 'merge_requests')
+ end
+
+ it 'does not enqueue ParallelProjectExportWorker and re-enqueue WaitRelationExportsWorker' do
+ expect { described_class.new.perform(*job_args) }
+ .to change { Projects::ImportExport::ParallelProjectExportWorker.jobs.size }.by(0)
+ .and change { described_class.jobs.size }.by(1)
+ end
+ end
+
+ context 'when there are relation exports with status `started`' do
+ let(:started_relation_export) { create_relation_export(:started, 'releases') }
+
+ before do
+ create_relation_export(:finished, 'labels')
+ create_relation_export(:queued, 'merge_requests')
+ end
+
+ context 'when the Sidekiq Job exporting the relation is still running' do
+ it "does not change relation export's status and re-enqueue WaitRelationExportsWorker" do
+ allow(Gitlab::SidekiqStatus).to receive(:running?).with(started_relation_export.jid).and_return(true)
+
+ expect { described_class.new.perform(*job_args) }
+ .to change { described_class.jobs.size }.by(1)
+
+ expect(started_relation_export.reload.started?).to eq(true)
+ end
+ end
+
+ context 'when the Sidekiq Job exporting the relation is still is no longer running' do
+ it "set the relation export's status to `failed`" do
+ allow(Gitlab::SidekiqStatus).to receive(:running?).with(started_relation_export.jid).and_return(false)
+
+ expect { described_class.new.perform(*job_args) }
+ .to change { described_class.jobs.size }.by(1)
+
+ expect(started_relation_export.reload.failed?).to eq(true)
+ end
+ end
+ end
+
+ context 'when all relation exports have status `finished`' do
+ before do
+ create_relation_export(:finished, 'labels')
+ create_relation_export(:finished, 'issues')
+ end
+
+ it 'enqueues ParallelProjectExportWorker and does not reenqueue WaitRelationExportsWorker' do
+ expect { described_class.new.perform(*job_args) }
+ .to change { Projects::ImportExport::ParallelProjectExportWorker.jobs.size }.by(1)
+ .and change { described_class.jobs.size }.by(0)
+ end
+
+ it_behaves_like 'an idempotent worker'
+ end
+
+ context 'when at least one relation export has status `failed` and the rest have status `finished` or `failed`' do
+ before do
+ create_relation_export(:finished, 'labels')
+ create_relation_export(:failed, 'issues', 'Failed to export issues')
+ create_relation_export(:failed, 'releases', 'Failed to export releases')
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ it 'notifies the failed exports to the user' do
+ expect_next_instance_of(NotificationService) do |notification_service|
+ expect(notification_service).to receive(:project_not_exported)
+ .with(
+ project_export_job.project,
+ user,
+ array_including(['Failed to export issues', 'Failed to export releases'])
+ )
+ .once
+ end
+
+ described_class.new.perform(*job_args)
+ end
+ end
+
+ it 'does not enqueue ParallelProjectExportWorker and re-enqueue WaitRelationExportsWorker' do
+ expect { described_class.new.perform(*job_args) }
+ .to change { Projects::ImportExport::ParallelProjectExportWorker.jobs.size }.by(0)
+ .and change { described_class.jobs.size }.by(0)
+ 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 f3c6434dc85..15234827efa 100644
--- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
+++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::InactiveProjectsDeletionCronWorker do
+RSpec.describe Projects::InactiveProjectsDeletionCronWorker, feature_category: :compliance_management do
include ProjectHelpers
shared_examples 'worker is running for more than 4 minutes' do
diff --git a/spec/workers/projects/inactive_projects_deletion_notification_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_notification_worker_spec.rb
index 3ddfec0d346..28668188497 100644
--- a/spec/workers/projects/inactive_projects_deletion_notification_worker_spec.rb
+++ b/spec/workers/projects/inactive_projects_deletion_notification_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::InactiveProjectsDeletionNotificationWorker do
+RSpec.describe Projects::InactiveProjectsDeletionNotificationWorker, feature_category: :compliance_management do
describe "#perform" do
subject(:worker) { described_class.new }
diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb
index b702eed9ea4..2c50a07cf48 100644
--- a/spec/workers/projects/post_creation_worker_spec.rb
+++ b/spec/workers/projects/post_creation_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PostCreationWorker do
+RSpec.describe Projects::PostCreationWorker, feature_category: :source_code_management do
let_it_be(:user) { create :user }
let(:worker) { described_class.new }
diff --git a/spec/workers/projects/process_sync_events_worker_spec.rb b/spec/workers/projects/process_sync_events_worker_spec.rb
index a10a4797b2c..77ccf14a32b 100644
--- a/spec/workers/projects/process_sync_events_worker_spec.rb
+++ b/spec/workers/projects/process_sync_events_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ProcessSyncEventsWorker do
+RSpec.describe Projects::ProcessSyncEventsWorker, feature_category: :pods do
let!(:group) { create(:group) }
let!(:project) { create(:project) }
diff --git a/spec/workers/projects/record_target_platforms_worker_spec.rb b/spec/workers/projects/record_target_platforms_worker_spec.rb
index 01852f252b7..c1e99d52473 100644
--- a/spec/workers/projects/record_target_platforms_worker_spec.rb
+++ b/spec/workers/projects/record_target_platforms_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RecordTargetPlatformsWorker do
+RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :projects do
include ExclusiveLeaseHelpers
let_it_be(:swift) { create(:programming_language, name: 'Swift') }
diff --git a/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb b/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb
index 99627ff1ad2..f2b7e75fa10 100644
--- a/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb
+++ b/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsWorker do
+RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsWorker, feature_category: :build_artifacts do
let(:worker) { described_class.new }
describe '#perform_work' do
diff --git a/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
index 7eff8e4dcd7..1d328280389 100644
--- a/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
+++ b/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ScheduleBulkRepositoryShardMovesWorker do
+RSpec.describe Projects::ScheduleBulkRepositoryShardMovesWorker, feature_category: :gitaly do
it_behaves_like 'schedules bulk repository shard moves' do
let_it_be_with_reload(:container) { create(:project, :repository) }
diff --git a/spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb b/spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb
index b5775f37678..b2111b2efb0 100644
--- a/spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb
+++ b/spec/workers/projects/schedule_refresh_build_artifacts_size_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ScheduleRefreshBuildArtifactsSizeStatisticsWorker do
+RSpec.describe Projects::ScheduleRefreshBuildArtifactsSizeStatisticsWorker, feature_category: :build_artifacts do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/projects/update_repository_storage_worker_spec.rb b/spec/workers/projects/update_repository_storage_worker_spec.rb
index 7570d706325..91445c2bbf6 100644
--- a/spec/workers/projects/update_repository_storage_worker_spec.rb
+++ b/spec/workers/projects/update_repository_storage_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::UpdateRepositoryStorageWorker do
+RSpec.describe Projects::UpdateRepositoryStorageWorker, feature_category: :source_code_management do
subject { described_class.new }
it_behaves_like 'an update storage move worker' do
diff --git a/spec/workers/propagate_integration_group_worker_spec.rb b/spec/workers/propagate_integration_group_worker_spec.rb
index 60442438a1d..0d797d22137 100644
--- a/spec/workers/propagate_integration_group_worker_spec.rb
+++ b/spec/workers/propagate_integration_group_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PropagateIntegrationGroupWorker do
+RSpec.describe PropagateIntegrationGroupWorker, feature_category: :integrations do
describe '#perform' do
let_it_be(:group) { create(:group) }
let_it_be(:another_group) { create(:group) }
diff --git a/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb b/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb
index c9a7bfaa8b6..d69dd45a209 100644
--- a/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb
+++ b/spec/workers/propagate_integration_inherit_descendant_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PropagateIntegrationInheritDescendantWorker do
+RSpec.describe PropagateIntegrationInheritDescendantWorker, feature_category: :integrations do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:group_integration) { create(:redmine_integration, :group, group: group) }
diff --git a/spec/workers/propagate_integration_inherit_worker_spec.rb b/spec/workers/propagate_integration_inherit_worker_spec.rb
index dd5d246d7f9..f5535696fd1 100644
--- a/spec/workers/propagate_integration_inherit_worker_spec.rb
+++ b/spec/workers/propagate_integration_inherit_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PropagateIntegrationInheritWorker do
+RSpec.describe PropagateIntegrationInheritWorker, feature_category: :integrations do
describe '#perform' do
let_it_be(:integration) { create(:redmine_integration, :instance) }
let_it_be(:integration1) { create(:redmine_integration, inherit_from_id: integration.id) }
diff --git a/spec/workers/propagate_integration_worker_spec.rb b/spec/workers/propagate_integration_worker_spec.rb
index 030caefb833..90134b5cd64 100644
--- a/spec/workers/propagate_integration_worker_spec.rb
+++ b/spec/workers/propagate_integration_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PropagateIntegrationWorker do
+RSpec.describe PropagateIntegrationWorker, feature_category: :integrations do
describe '#perform' do
let(:project) { create(:project) }
let(:integration) do
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
index c1ba9128475..8046fff2cf3 100644
--- a/spec/workers/prune_old_events_worker_spec.rb
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PruneOldEventsWorker do
+RSpec.describe PruneOldEventsWorker, feature_category: :user_profile do
describe '#perform' do
let(:user) { create(:user) }
@@ -30,5 +30,17 @@ RSpec.describe PruneOldEventsWorker do
subject.perform
expect(not_expired_3_years_event).to be_present
end
+
+ context 'with ops_prune_old_events FF disabled' do
+ before do
+ stub_feature_flags(ops_prune_old_events: false)
+ end
+
+ it 'does not delete' do
+ subject.perform
+
+ expect(Event.find_by(id: expired_event.id)).to be_present
+ end
+ end
end
end
diff --git a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
index 84315fd6ee9..49ef73bad53 100644
--- a/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
+++ b/spec/workers/purge_dependency_proxy_cache_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PurgeDependencyProxyCacheWorker do
+RSpec.describe PurgeDependencyProxyCacheWorker, feature_category: :dependency_proxy do
let_it_be(:user) { create(:admin) }
let_it_be_with_refind(:blob) { create(:dependency_proxy_blob ) }
let_it_be_with_reload(:group) { blob.group }
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
index 63b26817a7a..4e9d638c1e1 100644
--- a/spec/workers/reactive_caching_worker_spec.rb
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -2,6 +2,6 @@
require 'spec_helper'
-RSpec.describe ReactiveCachingWorker do
+RSpec.describe ReactiveCachingWorker, feature_category: :shared do
it_behaves_like 'reactive cacheable worker'
end
diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb
index 4bdfd7219f2..eec221094e6 100644
--- a/spec/workers/rebase_worker_spec.rb
+++ b/spec/workers/rebase_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RebaseWorker, '#perform' do
+RSpec.describe RebaseWorker, '#perform', feature_category: :source_code_management do
include ProjectForksHelper
context 'when rebasing an MR from a fork where upstream has protected branches' do
diff --git a/spec/workers/releases/create_evidence_worker_spec.rb b/spec/workers/releases/create_evidence_worker_spec.rb
index 7e3edcfe44a..8631b154920 100644
--- a/spec/workers/releases/create_evidence_worker_spec.rb
+++ b/spec/workers/releases/create_evidence_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::CreateEvidenceWorker do
+RSpec.describe Releases::CreateEvidenceWorker, feature_category: :release_evidence do
let(:project) { create(:project, :repository) }
let(:release) { create(:release, project: project) }
let(:pipeline) { create(:ci_empty_pipeline, sha: release.sha, project: project) }
diff --git a/spec/workers/releases/manage_evidence_worker_spec.rb b/spec/workers/releases/manage_evidence_worker_spec.rb
index 0004a4f4bfb..ca33e28b760 100644
--- a/spec/workers/releases/manage_evidence_worker_spec.rb
+++ b/spec/workers/releases/manage_evidence_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::ManageEvidenceWorker do
+RSpec.describe Releases::ManageEvidenceWorker, feature_category: :release_evidence do
let(:project) { create(:project, :repository) }
shared_examples_for 'does not create a new Evidence record' do
diff --git a/spec/workers/remote_mirror_notification_worker_spec.rb b/spec/workers/remote_mirror_notification_worker_spec.rb
index e415e72645c..46f44d0047b 100644
--- a/spec/workers/remote_mirror_notification_worker_spec.rb
+++ b/spec/workers/remote_mirror_notification_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RemoteMirrorNotificationWorker, :mailer do
+RSpec.describe RemoteMirrorNotificationWorker, :mailer, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository, :remote_mirror) }
let_it_be(:mirror) { project.remote_mirrors.first }
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
index 7bdf6fc0d59..e08cc3fb5c5 100644
--- a/spec/workers/remove_expired_group_links_worker_spec.rb
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RemoveExpiredGroupLinksWorker do
+RSpec.describe RemoveExpiredGroupLinksWorker, feature_category: :system_access do
describe '#perform' do
context 'ProjectGroupLinks' do
let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
index 062a9bcfa83..354ce3fc9b4 100644
--- a/spec/workers/remove_expired_members_worker_spec.rb
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RemoveExpiredMembersWorker do
+RSpec.describe RemoveExpiredMembersWorker, feature_category: :system_access do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/remove_unaccepted_member_invites_worker_spec.rb b/spec/workers/remove_unaccepted_member_invites_worker_spec.rb
index 96d7cf535ed..97ddf9223b3 100644
--- a/spec/workers/remove_unaccepted_member_invites_worker_spec.rb
+++ b/spec/workers/remove_unaccepted_member_invites_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RemoveUnacceptedMemberInvitesWorker do
+RSpec.describe RemoveUnacceptedMemberInvitesWorker, feature_category: :system_access do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
index 2562a7bc6fe..56dc3511cfc 100644
--- a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RemoveUnreferencedLfsObjectsWorker do
+RSpec.describe RemoveUnreferencedLfsObjectsWorker, feature_category: :source_code_management do
let(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 643b55af573..bb4a4844b9b 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryCheck::BatchWorker do
+RSpec.describe RepositoryCheck::BatchWorker, feature_category: :source_code_management do
let(:shard_name) { 'default' }
subject { described_class.new }
diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb
index b5f09e8a05f..dd11a2705ac 100644
--- a/spec/workers/repository_check/clear_worker_spec.rb
+++ b/spec/workers/repository_check/clear_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryCheck::ClearWorker do
+RSpec.describe RepositoryCheck::ClearWorker, feature_category: :source_code_management do
it 'clears repository check columns' do
project = create(:project)
project.update_columns(
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
index 829abc7d895..34ecc645675 100644
--- a/spec/workers/repository_check/dispatch_worker_spec.rb
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryCheck::DispatchWorker do
+RSpec.describe RepositoryCheck::DispatchWorker, feature_category: :source_code_management do
subject { described_class.new }
it 'does nothing when repository checks are disabled' do
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 0a37a296e7a..3e52ce781ee 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'fileutils'
-RSpec.describe RepositoryCheck::SingleRepositoryWorker do
+RSpec.describe RepositoryCheck::SingleRepositoryWorker, feature_category: :source_code_management do
subject(:worker) { described_class.new }
before do
diff --git a/spec/workers/repository_cleanup_worker_spec.rb b/spec/workers/repository_cleanup_worker_spec.rb
index 2b700b944d2..1e80a48451b 100644
--- a/spec/workers/repository_cleanup_worker_spec.rb
+++ b/spec/workers/repository_cleanup_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryCleanupWorker do
+RSpec.describe RepositoryCleanupWorker, feature_category: :source_code_management do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 85dee935001..3a5528b6a04 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryForkWorker do
+RSpec.describe RepositoryForkWorker, feature_category: :source_code_management do
include ProjectForksHelper
describe 'modules' do
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
index ef6a8d76d2c..c1987658b0d 100644
--- a/spec/workers/repository_update_remote_mirror_worker_spec.rb
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state, feature_category: :source_code_management do
let_it_be(:remote_mirror) { create(:remote_mirror) }
let(:scheduled_time) { Time.current - 5.minutes }
diff --git a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
index 49730d9ab8c..b93202fe9b3 100644
--- a/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
+++ b/spec/workers/schedule_merge_request_cleanup_refs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ScheduleMergeRequestCleanupRefsWorker do
+RSpec.describe ScheduleMergeRequestCleanupRefsWorker, feature_category: :code_review_workflow do
subject(:worker) { described_class.new }
describe '#perform' do
diff --git a/spec/workers/schedule_migrate_external_diffs_worker_spec.rb b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb
index 09a0124f6e0..061467a28e0 100644
--- a/spec/workers/schedule_migrate_external_diffs_worker_spec.rb
+++ b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ScheduleMigrateExternalDiffsWorker do
+RSpec.describe ScheduleMigrateExternalDiffsWorker, feature_category: :code_review_workflow do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/self_monitoring_project_create_worker_spec.rb b/spec/workers/self_monitoring_project_create_worker_spec.rb
index b618b8ede99..cc31804acc1 100644
--- a/spec/workers/self_monitoring_project_create_worker_spec.rb
+++ b/spec/workers/self_monitoring_project_create_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SelfMonitoringProjectCreateWorker do
+RSpec.describe SelfMonitoringProjectCreateWorker, feature_category: :projects do
describe '#perform' do
let(:service_class) { Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService }
let(:service) { instance_double(service_class) }
diff --git a/spec/workers/self_monitoring_project_delete_worker_spec.rb b/spec/workers/self_monitoring_project_delete_worker_spec.rb
index 9a53fe59a40..808f65a25bd 100644
--- a/spec/workers/self_monitoring_project_delete_worker_spec.rb
+++ b/spec/workers/self_monitoring_project_delete_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SelfMonitoringProjectDeleteWorker do
+RSpec.describe SelfMonitoringProjectDeleteWorker, feature_category: :projects do
let_it_be(:jid) { 'b5b28910d97563e58c2fe55f' }
let_it_be(:data_key) { "self_monitoring_delete_result:#{jid}" }
diff --git a/spec/workers/service_desk_email_receiver_worker_spec.rb b/spec/workers/service_desk_email_receiver_worker_spec.rb
index 60fc951f627..bed66875f34 100644
--- a/spec/workers/service_desk_email_receiver_worker_spec.rb
+++ b/spec/workers/service_desk_email_receiver_worker_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do
+RSpec.describe ServiceDeskEmailReceiverWorker, :mailer, feature_category: :service_desk do
describe '#perform' do
let(:worker) { described_class.new }
let(:email) { fixture_file('emails/service_desk_custom_address.eml') }
diff --git a/spec/workers/snippets/schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/snippets/schedule_bulk_repository_shard_moves_worker_spec.rb
index be7d8ebe2d3..d31600d9cba 100644
--- a/spec/workers/snippets/schedule_bulk_repository_shard_moves_worker_spec.rb
+++ b/spec/workers/snippets/schedule_bulk_repository_shard_moves_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::ScheduleBulkRepositoryShardMovesWorker do
+RSpec.describe Snippets::ScheduleBulkRepositoryShardMovesWorker, feature_category: :gitaly do
it_behaves_like 'schedules bulk repository shard moves' do
let_it_be_with_reload(:container) { create(:snippet, :repository).tap { |snippet| snippet.create_repository } }
diff --git a/spec/workers/snippets/update_repository_storage_worker_spec.rb b/spec/workers/snippets/update_repository_storage_worker_spec.rb
index 38e9012e9c5..b26384fc75c 100644
--- a/spec/workers/snippets/update_repository_storage_worker_spec.rb
+++ b/spec/workers/snippets/update_repository_storage_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Snippets::UpdateRepositoryStorageWorker do
+RSpec.describe Snippets::UpdateRepositoryStorageWorker, feature_category: :source_code_management do
subject { described_class.new }
it_behaves_like 'an update storage move worker' do
diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
index 26d9460d73e..f3ba586c21e 100644
--- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
@@ -2,12 +2,11 @@
require 'spec_helper'
-RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do
+RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker, feature_category: :compliance_management do
subject(:worker) { described_class.new }
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..f6eaf76b54d 100644
--- a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
@@ -2,12 +2,11 @@
require 'spec_helper'
-RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do
+RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker, feature_category: :compliance_management do
subject(:worker) { described_class.new }
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/stage_update_worker_spec.rb b/spec/workers/stage_update_worker_spec.rb
index e50c7183153..bb2f63d3b50 100644
--- a/spec/workers/stage_update_worker_spec.rb
+++ b/spec/workers/stage_update_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe StageUpdateWorker do
+RSpec.describe StageUpdateWorker, feature_category: :continuous_integration do
describe '#perform' do
context 'when stage exists' do
let(:stage) { create(:ci_stage) }
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index 19ff8ec55c2..1c5f54dc55f 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe StuckCiJobsWorker do
+RSpec.describe StuckCiJobsWorker, feature_category: :continuous_integration do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/stuck_export_jobs_worker_spec.rb b/spec/workers/stuck_export_jobs_worker_spec.rb
index cbc7adc8e3f..0e300b0077b 100644
--- a/spec/workers/stuck_export_jobs_worker_spec.rb
+++ b/spec/workers/stuck_export_jobs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe StuckExportJobsWorker do
+RSpec.describe StuckExportJobsWorker, feature_category: :importers do
let(:worker) { described_class.new }
shared_examples 'project export job detection' do
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index bade2e1ca1b..44dc6550cdb 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe StuckMergeJobsWorker do
+RSpec.describe StuckMergeJobsWorker, feature_category: :code_review_workflow do
describe 'perform' do
let(:worker) { described_class.new }
diff --git a/spec/workers/system_hook_push_worker_spec.rb b/spec/workers/system_hook_push_worker_spec.rb
index 43a3f8e3e19..35e44e635cc 100644
--- a/spec/workers/system_hook_push_worker_spec.rb
+++ b/spec/workers/system_hook_push_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemHookPushWorker do
+RSpec.describe SystemHookPushWorker, feature_category: :source_code_management do
include RepoHelpers
subject { described_class.new }
diff --git a/spec/workers/tasks_to_be_done/create_worker_spec.rb b/spec/workers/tasks_to_be_done/create_worker_spec.rb
index c3c3612f9a7..643424ae068 100644
--- a/spec/workers/tasks_to_be_done/create_worker_spec.rb
+++ b/spec/workers/tasks_to_be_done/create_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TasksToBeDone::CreateWorker do
+RSpec.describe TasksToBeDone::CreateWorker, feature_category: :onboarding do
let_it_be(:member_task) { create(:member_task, tasks: MemberTask::TASKS.values) }
let_it_be(:current_user) { create(:user) }
diff --git a/spec/workers/terraform/states/destroy_worker_spec.rb b/spec/workers/terraform/states/destroy_worker_spec.rb
index 02e79373279..7cc20e0ad5d 100644
--- a/spec/workers/terraform/states/destroy_worker_spec.rb
+++ b/spec/workers/terraform/states/destroy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Terraform::States::DestroyWorker do
+RSpec.describe Terraform::States::DestroyWorker, feature_category: :infrastructure_as_code do
let(:state) { create(:terraform_state) }
describe '#perform' do
diff --git a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
index 86202fac1ed..54e2061217f 100644
--- a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
+++ b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::ConfidentialIssueWorker do
+RSpec.describe TodosDestroyer::ConfidentialIssueWorker, feature_category: :team_planning do
let(:service) { double }
it "calls the Todos::Destroy::ConfidentialIssueService with issue_id parameter" do
diff --git a/spec/workers/todos_destroyer/destroyed_designs_worker_spec.rb b/spec/workers/todos_destroyer/destroyed_designs_worker_spec.rb
index 113faeb0d2f..b85ea1a5847 100644
--- a/spec/workers/todos_destroyer/destroyed_designs_worker_spec.rb
+++ b/spec/workers/todos_destroyer/destroyed_designs_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::DestroyedDesignsWorker do
+RSpec.describe TodosDestroyer::DestroyedDesignsWorker, feature_category: :team_planning do
let(:service) { double }
it 'calls the Todos::Destroy::DesignService with design_ids parameter' do
diff --git a/spec/workers/todos_destroyer/destroyed_issuable_worker_spec.rb b/spec/workers/todos_destroyer/destroyed_issuable_worker_spec.rb
index 6ccad25ad76..72adc7ba0c7 100644
--- a/spec/workers/todos_destroyer/destroyed_issuable_worker_spec.rb
+++ b/spec/workers/todos_destroyer/destroyed_issuable_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::DestroyedIssuableWorker do
+RSpec.describe TodosDestroyer::DestroyedIssuableWorker, feature_category: :team_planning do
let(:job_args) { [1, 'MergeRequest'] }
it 'calls the Todos::Destroy::DestroyedIssuableService' do
diff --git a/spec/workers/todos_destroyer/entity_leave_worker_spec.rb b/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
index db3b0252056..d7682ad0e5e 100644
--- a/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
+++ b/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::EntityLeaveWorker do
+RSpec.describe TodosDestroyer::EntityLeaveWorker, feature_category: :team_planning do
it "calls the Todos::Destroy::EntityLeaveService with the params it was given" do
service = double
diff --git a/spec/workers/todos_destroyer/group_private_worker_spec.rb b/spec/workers/todos_destroyer/group_private_worker_spec.rb
index 4903edd4bbe..4d49a852a3e 100644
--- a/spec/workers/todos_destroyer/group_private_worker_spec.rb
+++ b/spec/workers/todos_destroyer/group_private_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::GroupPrivateWorker do
+RSpec.describe TodosDestroyer::GroupPrivateWorker, feature_category: :team_planning do
it "calls the Todos::Destroy::GroupPrivateService with the params it was given" do
service = double
diff --git a/spec/workers/todos_destroyer/private_features_worker_spec.rb b/spec/workers/todos_destroyer/private_features_worker_spec.rb
index 88d9be051d0..834e0fb2201 100644
--- a/spec/workers/todos_destroyer/private_features_worker_spec.rb
+++ b/spec/workers/todos_destroyer/private_features_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::PrivateFeaturesWorker do
+RSpec.describe TodosDestroyer::PrivateFeaturesWorker, feature_category: :team_planning do
it "calls the Todos::Destroy::PrivateFeaturesService with the params it was given" do
service = double
diff --git a/spec/workers/todos_destroyer/project_private_worker_spec.rb b/spec/workers/todos_destroyer/project_private_worker_spec.rb
index 4e54fbdb275..2435fcade22 100644
--- a/spec/workers/todos_destroyer/project_private_worker_spec.rb
+++ b/spec/workers/todos_destroyer/project_private_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosDestroyer::ProjectPrivateWorker do
+RSpec.describe TodosDestroyer::ProjectPrivateWorker, feature_category: :team_planning do
it "calls the Todos::Destroy::ProjectPrivateService with the params it was given" do
service = double
diff --git a/spec/workers/trending_projects_worker_spec.rb b/spec/workers/trending_projects_worker_spec.rb
index 1f1e312e457..b6acd01f7c4 100644
--- a/spec/workers/trending_projects_worker_spec.rb
+++ b/spec/workers/trending_projects_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TrendingProjectsWorker do
+RSpec.describe TrendingProjectsWorker, feature_category: :source_code_management do
describe '#perform' do
it 'refreshes the trending projects' do
expect(TrendingProject).to receive(:refresh!)
diff --git a/spec/workers/update_container_registry_info_worker_spec.rb b/spec/workers/update_container_registry_info_worker_spec.rb
index ace9e55cbce..a8c501efd51 100644
--- a/spec/workers/update_container_registry_info_worker_spec.rb
+++ b/spec/workers/update_container_registry_info_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateContainerRegistryInfoWorker do
+RSpec.describe UpdateContainerRegistryInfoWorker, feature_category: :container_registry do
describe '#perform' do
it 'calls UpdateContainerRegistryInfoService' do
expect_next_instance_of(UpdateContainerRegistryInfoService) do |service|
diff --git a/spec/workers/update_external_pull_requests_worker_spec.rb b/spec/workers/update_external_pull_requests_worker_spec.rb
index cb6a4e2ebf8..254a4b69f3b 100644
--- a/spec/workers/update_external_pull_requests_worker_spec.rb
+++ b/spec/workers/update_external_pull_requests_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateExternalPullRequestsWorker do
+RSpec.describe UpdateExternalPullRequestsWorker, feature_category: :continuous_integration do
describe '#perform' do
let_it_be(:project) { create(:project, import_source: 'tanuki/repository') }
let_it_be(:user) { create(:user) }
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
index 5ed600e308b..5eae275be36 100644
--- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateHeadPipelineForMergeRequestWorker do
+RSpec.describe UpdateHeadPipelineForMergeRequestWorker, feature_category: :continuous_integration do
describe '#perform' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/workers/update_highest_role_worker_spec.rb b/spec/workers/update_highest_role_worker_spec.rb
index cd127f26e95..94811260f0e 100644
--- a/spec/workers/update_highest_role_worker_spec.rb
+++ b/spec/workers/update_highest_role_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do
+RSpec.describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state, feature_category: :subscription_cost_management do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 64fcc2bd388..de164fe352a 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateMergeRequestsWorker do
+RSpec.describe UpdateMergeRequestsWorker, feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:oldrev) { "123456" }
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
index 2f356376d7c..c5e6f45a201 100644
--- a/spec/workers/update_project_statistics_worker_spec.rb
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UpdateProjectStatisticsWorker do
+RSpec.describe UpdateProjectStatisticsWorker, feature_category: :source_code_management do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
index 75d7509b6e6..6c47a8e198c 100644
--- a/spec/workers/upload_checksum_worker_spec.rb
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UploadChecksumWorker do
+RSpec.describe UploadChecksumWorker, feature_category: :geo_replication do
describe '#perform' do
subject { described_class.new }
diff --git a/spec/workers/user_status_cleanup/batch_worker_spec.rb b/spec/workers/user_status_cleanup/batch_worker_spec.rb
index 2fd84d0e085..e2ad12672de 100644
--- a/spec/workers/user_status_cleanup/batch_worker_spec.rb
+++ b/spec/workers/user_status_cleanup/batch_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe UserStatusCleanup::BatchWorker do
+RSpec.describe UserStatusCleanup::BatchWorker, feature_category: :user_profile do
include_examples 'an idempotent worker' do
subject do
perform_multiple([], worker: described_class.new)
diff --git a/spec/workers/users/create_statistics_worker_spec.rb b/spec/workers/users/create_statistics_worker_spec.rb
index 2118cc42f3a..2c1c7738533 100644
--- a/spec/workers/users/create_statistics_worker_spec.rb
+++ b/spec/workers/users/create_statistics_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::CreateStatisticsWorker do
+RSpec.describe Users::CreateStatisticsWorker, feature_category: :user_profile do
describe '#perform' do
subject { described_class.new.perform }
diff --git a/spec/workers/users/deactivate_dormant_users_worker_spec.rb b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
index a8318de669b..1fb936b1fc2 100644
--- a/spec/workers/users/deactivate_dormant_users_worker_spec.rb
+++ b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::DeactivateDormantUsersWorker do
+RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :subscription_cost_management do
using RSpec::Parameterized::TableSyntax
describe '#perform' do
@@ -12,8 +12,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
subject(:worker) { described_class.new }
- it 'does not run for GitLab.com' do
- expect(Gitlab).to receive(:com?).and_return(true)
+ it 'does not run for SaaS', :saas do
# Now makes a call to current settings to determine period of dormancy
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
index 7c585542e30..73faffb5387 100644
--- 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::MigrateRecordsToGhostUserInBatchesWorker do
+RSpec.describe Users::MigrateRecordsToGhostUserInBatchesWorker, feature_category: :subscription_cost_management do
include ExclusiveLeaseHelpers
let(:worker) { described_class.new }
diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb
index e2ff36975c4..e39017c4ccf 100644
--- a/spec/workers/web_hook_worker_spec.rb
+++ b/spec/workers/web_hook_worker_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe WebHookWorker do
+RSpec.describe WebHookWorker, feature_category: :integrations do
include AfterNextHelpers
let_it_be(:project_hook) { create(:project_hook) }
diff --git a/spec/workers/web_hooks/log_destroy_worker_spec.rb b/spec/workers/web_hooks/log_destroy_worker_spec.rb
index 0c107c05360..8c7d29d559d 100644
--- a/spec/workers/web_hooks/log_destroy_worker_spec.rb
+++ b/spec/workers/web_hooks/log_destroy_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHooks::LogDestroyWorker do
+RSpec.describe WebHooks::LogDestroyWorker, feature_category: :integrations do
include AfterNextHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/workers/x509_certificate_revoke_worker_spec.rb b/spec/workers/x509_certificate_revoke_worker_spec.rb
index 392cb52d084..badeff25b34 100644
--- a/spec/workers/x509_certificate_revoke_worker_spec.rb
+++ b/spec/workers/x509_certificate_revoke_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe X509CertificateRevokeWorker do
+RSpec.describe X509CertificateRevokeWorker, feature_category: :source_code_management do
describe '#perform' do
context 'with a revoked certificate' do
subject { described_class.new }
diff --git a/spec/workers/x509_issuer_crl_check_worker_spec.rb b/spec/workers/x509_issuer_crl_check_worker_spec.rb
index 5564147d274..55e2e8d335d 100644
--- a/spec/workers/x509_issuer_crl_check_worker_spec.rb
+++ b/spec/workers/x509_issuer_crl_check_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe X509IssuerCrlCheckWorker do
+RSpec.describe X509IssuerCrlCheckWorker, feature_category: :source_code_management do
subject(:worker) { described_class.new }
let(:project) { create(:project, :public, :repository) }